Writing Custom Iterators For Prototype

—Wednesday, July 05 2006

Traversing the DOM can be a painful beast at times, but we can remove a lot of that pain by writing a simple set of iterators that will allow us to pick it apart at will.

When you ask for the childNodes of an element, most of the time you’re just wanting the element nodes only and you’d like to completely ignore the text nodes. Using Prototype, you could write something like the code below:

element = $(element);
element.cleanWhitespace();
$A(element.childNodes).each(function(element) {
  element.setStyle({color: '#ccc'});
});

That would be a quick fix, but remember, we’re not supposed to be repeating ourselves. I find myself wanting to perform a task like this time and time again. Basically the code above just grabs a parent element, cleans the whitespace from it (removes the text nodes) and gives us back only the elements which we wanted to begin with. It’s much simpler to DRY up this process by implementing our own custom iterator.

The eachElement Method

We can easily reuse the code above by appending an eachElement method to the Element.Methods object.

Element.addMethods({
  eachElement: function(element, iterator) {
    element = $(element);
    element.cleanWhitespace();
    $A(element.childNodes).each(iterator);
  }
});

Now that we’ve got this in place, it’ll make our life a lot easier, our code a lot less redundant, and anyone who reads our code will probably have a better understanding of what we’re doing. Never mind the fact it just rocks being able to do something like this.

  $('options').eachElement(function(element) {
    console.log(element);
  });

Tag-based Iterators

The eachElement method is very nice by itself, but what if we’re still repeating ourselves? Maybe we only want the form elements inside a div? We could use our eachElement method and filter out elements by their tag name.

  $('options').eachElement(function(element) {
    if(element.tagName.toLowerCase() == 'form')
      //Do stuff with forms
  });

We don’t want to have to write these conditional checks inside our eachElement method every time we want to filter out a certain tag, but at the same time, we don’t want to write a method for every tag available to an HTML document, nor do we want to have a switch statement that checks for every tag. With this in mind, what’s the most effective way to achieve tag-based iterators? We’ll dynamically add methods at runtime.

Basically, we want to write a method that writes methods and we want this method to execute as soon as our Javascript is loaded. We want to dynamically create methods such as eachDiv, eachLi, eachSelect, etc. So lets look at the code to make this possible:

var Iterators = function() {
  var tags = "div p span ul ol li span form input select textarea h1 h2 h3 h4 h5 h6 dl dt em strong";
  var methods = {};
  $A(tags.split(' ')).each(function(tag) {
    methods["each" + tag.charAt(0).toUpperCase() + tag.substring(1)] = function(element, iterator) {
      element = $(element);
      element.cleanWhitespace();
      $A(element.getElementsByTagName(tag)).each(iterator);
    }
  });
  
  Element.addMethods(methods);
}();

If you look at the code, you’ll notice the list of tags we have. Now I don’t have all the tags here, but enough for you to get the point. After we have our tag list, we can create an object to hold our methods, and then dynamically start building our iterators.

The line below is where the magic happens:

methods["each" + tag.charAt(0).toUpperCase() + tag.substring(1)] = ...

Considering I want to follow the camelCase nature of Javascript convention, I’m breaking off the first character and up-casing it. So instead of creating methods like eachselect, I’ll get eachSelect. Prototype has a camelize method of the String object, but it doesn’t work unless there is an underscore or dash in the name.

When we’ve built our method object up, we can now assign these methods to the Element.Methods object using Element.addMethods. Once this is run we can now do some pretty cool stuff like:

$('list').eachLi(function(item) {
  item.setStyle({color: '#ccc'});
});

The one funky thing you might notice is the last line. By adding the parentheses we create a self-invoking function. This function automatically executes after it’s loaded, thus creating our methods before we need to use them.

So there it is, pretty short, pretty sweet, extremely handy. You could certainly build some really cool stuff using this type of dynamic method generation. Until next time!