Go back to the main page

Refactoring your code into the event delegation pattern in Prototype

 

Standard event pattern

In this example we attach onclick behaviors for the .more anchor tags. When these tags are clicked a certain part of the paragraph will become highlighted. The function highlightMoreTexts is the registered event handler for the onclick behavior. Inside of highlightMoreTexts the this variable references the element that is clicked on. That is because observe will register event handlers with this bound to the currentTarget property.

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <title>Refactor to event delegation</title>
    <script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js" type="text/javascript"></script>
  </head>
  <body>
    <div class="row">
      <p>Incrementing nonalphanumerics <a href="#" class="more"><span>highlight..</span></a><span class="moreText">uses the underlying character set's collating sequence</span>.
      </p>
    </div>
    <div class="row">
      <p>Incrementing nonalphanumerics <a href="#" class="more"><span>highlight..</span></a><span class="moreText">uses the underlying character set's collating sequence</span>.
      </p>
    </div>
    <script>
      document.observe( 'dom:loaded',function(){
        function highlightMoreTexts(event){
            this.next('span').setStyle({ background: 'yellow' });
            event.stop();
        }

        $$('.more').invoke('observe','click',highlightMoreTexts);

      });
    </script>
  </body>
</html>


Put into a event delegation pattern and be done with it

Now let's take the first step in refactoring into a event delegation pattern. Below we will grab all of the clicks within the div.row tags. Using event.findElement we can figure out when a.more was actually the element clicked within div.row. Next, just stick in the highlightMoreTexts function call with a manual passing of the event object and we are done. Right?

        $$('div.row').invoke('observe','click',function(event){
            if(el = event.findElement('.more')){
                highlightMoreTexts(event);
            }
        });

Not so fast

This first attempt will not work because the this reference will be bound to the Window object. You may next decide to rewrite your handler to be more portable and take out the this reference and use event.target itself.


        function highlightMoreTexts(event){
            event.target.next('span').setStyle({ background: 'yellow' });
            event.stop();
        }

Still not done

Using event.target will work fine in the standard event pattern as detailed in the first paragraph but will again fail in the event delegation pattern. This is because there is a <span> tag inside of the <a> tag and findElement will match all events within the selector .more. Therefore, event.target inside of the highlightMoreTexts handler will be the span within the a.more tag.

Last step

Here is what we want to do and we don't have to rewrite any of our original event handler functions. In the code below, the el variable has the current extended element, meaning $(a.more). So, we take the el variable, which again references $(a.more) and bind it as the this scope for the highlightMoreTexts function. The next bind argument is the event object. Remember the first argument in the bind method on the function object will be the this scope for that function. After the first argument the rest of the arguments for bind will set arguments for the highlightMoreTexts function. Read more on Function.bind().

        /*
        * Use Function.bind() to refactor into the event delegation pattern
        * and you will not have to touch your existing event handlers
        */
        $$('div.row').invoke('observe','click',function(event){
            if(el = event.findElement('.more')){
                var fn = highlightMoreTexts.bind(el,event);
                fn();
            }
        });
  • Pushed on 06/03/2010 by Christian