July 5th, 2006

Writing Custom Iterators For Prototype

12 comments on 1167 words

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!

Discussion

  1. Martin Ström Martin Ström said on July 5th
    Great post. I should really clean up some code I’m working on right now and stop repeating myself repeating myself. One idea is just to pass the tagName as an argument instead. This wouldn’t be an example of custom iterators any longer but a slick way, like
    
    Element.addMethods({
      eachTag: function(element, tag, iterator) {
        element = $(element);
        element.cleanWhitespace();
        $A(element.getElementsByTagName(tag)).each(iterator);
      }
    });
    
    

    and use it like this:

    
    $("list").eachTag("li", function(element) {
      console.log(element);
    })
    
    
    Or have a eachElement() method that could accept a tag as argument, otherwise it gets all elements.
  2. Justin Justin said on July 5th

    Wow, that’s just about the slickest thing I’ve seen that you can do with Prototype!

    One question, this: $A(element.childNodes).each(iterator);

    Is there a hidden return going on there?

  3. Andrea Martines Andrea Martines said on July 5th

    I really wouldn’t spend this effort just to overwrite existing DOM statements.

    We already can iterate this way:

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

    But I suppose the Iterators argument is for you just the occasion for introducing dynamic method generation, that is far more interesting. And this is a great tutorial about it.

  4. Daniel Schierbeck Daniel Schierbeck said on July 6th

    Why not just jump the hoops and get Ruby into Firefox, Opera, and IE?

    I do appreciate that JavaScript is turning away from Java (as some lunatics had envisioned it could resemble) and in the direction of Ruby.

  5. Eric Anderson Eric Anderson said on July 6th

    I’m not sure that cleanWhitespace() does what you say it does. It will only remove text nodes that are pure whitespace. Depending on your markup you will probably have text nodes with real content. Here might be an alternate implementation that will do what you want.

    
    Element.addMethods({
      eachElement: function(element, iterator) {
        $A($(element).childNodes).reject(function(node) {return node.nodeType == 3}).each(iterator);
      }
    });
    
    
  6. Justin Palmer Justin Palmer said on July 6th

    @Justin: There isn’t a secret return. If your confused by the short hand syntax, your allowed to pass in a method that accepts whats being iterated as it’s first argument. It will automatically be passed as an argument to the iterator. Here is a more common example:

    
    
    $$('#list li').each(Element.hide);

    @Eric, your absolutely right. Thanks for catching that.

  7. Justin Perkins Justin Perkins said on July 6th

    Thanks Justin, I was actually confused because I thought we had a little ruby thing going on there (the last statement eval’d is returned), but now that I take a closer look I see that’s not going on at all.

    Thanks for clarifying :)

  8. Ismael Ismael said on July 7th

    Excellent post. I’m still amazed at how good Prototype actually is.

  9. Tobie Langel Tobie Langel said on July 9th

    Hi Justin,

    Reading your code and especially, the following lines:

    var tags = "div p span ul ol li span form input select textarea h1 h2 h3 h4 h5 h6 dl dt em strong";
    
    $A(tags.split(' ')).each(function(tag) { // I don't think the $A is necessary here, btw.
      // do something
    });

    - the like of which I’ve seen around elsewhere quite recently - I thaught (after discussion on irc) that Prototype could perhaps beneficiate from even more syntactic sugar by adding the following ruby flavored function:

    function $w(string){
      string = string.strip();
      return string ? string.split(/\s+/) : [];
    }

    making the following possible:

    
    $w('div p span ul').each(function(tag){
      alert(tag);
    });

    The only caveat being, of course, speed… any benchmarks around anyone?

    On a completely different topic: how’s your book going? Any deadlines for the first beta issue? I’m really looking forward to reading it.

    Another great article btw.

    Regards,

    Tobie

  10. Andrew Dupont Andrew Dupont said on July 10th

    Prototype has a camelize method of the String object, but it doesn’t work unless there is an underscore or dash in the name.

    I added a “capitalize” method to String.prototype a while back to solve this problem:

    
    String.prototype.capitalize = function() {
        return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
    };
    
  11. henrah henrah said on July 26th
    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.
    This is neat, but you are also assigning the value undefined to the varible Iterators because your function returns no value. Surely this assignment serves no purpose?
  12. Justin Palmer Justin Palmer said on July 30th

    henrah: You’ll get a syntax error if you don’t assign the function to a variable name.

Sorry, comments are closed for this article.