February 7th, 2006

Working With Events In Prototype

25 comments on 2539 words

Events drive interaction for almost everything and the web is no exception. In the style of my last article on Prototype, lets take a code-heavy look at how Prototype lets us work with events.

NOTE: This article uses Prototype 1.5.0_pre0

Basic event handling

The syntax for working with events looks like the code below.


Event.observe(element, name, observer, [useCapture]);

Assuming for a moment that we want to observe when a link was clicked, we could do the following:


// <a id="clicker" href="http://foo.com">Click me</a>
Event.observe('clicker', 'click', function(event){ alert('clicked!');});

If we wanted to get the element that fired the event, we’d do this:


Event.observe('clicker', 'click', function(event){ alert(Event.element(event));});

Observing keystrokes

If we wanted to observe keystrokes for the entire document, we could do the following:


Event.observe(document, 'keypress', function(event){ if(event.keyCode == Event.KEY_TAB) alert('Tab Pressed');});

And lets say we wanted to keep track of what has been typed into your snazzy live-search box:


Event.observe('search', 'keypress', function(event) { Element.update('search-results', $F(Event.element(event)));});

Prototype defines properties inside the event object for some of the more common keys, so feel free to dig around in Prototype to see which ones those are.

A final note on keypress events; If you’d like to detect a left click you can use Event.isLeftClick(event).

Getting the coordinates of the mouse pointer

Drag and drop, dynamic element resizing, games, and much more all require the ability to track the X and Y location of the mouse. Prototype makes this fairly simple. The code below tracks the X and Y position of the mouse and spits out those values into an input box named mouse.


   Event.observe(document, 'mousemove', function(event){$('mouse').value = "X: " + Event.pointerX(event) + "px Y: " + Event.pointerY(event) + "px";});

If we wanted to observe the mouse location when it was hovering over a certain element, we’d just change the document argument to the id or element that was relevant.

Stopping Propagation

Event.stop(event) will stop the propagation of an event but there is a bug in Safari 2.0.3 that causes this to behave unexpectedly. Ryan from Particletree has posted an in-depth article on Event.stop with a workaround for Safari.

The good news is that this bug is fixed in the Webkit version of Safari. You can check out this test in Safari 2.0.3 and Webkit to see the results of both.

Events, Binding, and Objects

Everything has been fairly straight forward so far, but things start getting a little tricker when you need to work with events in and object-oriented environment. You have to deal with binding and funky looking syntax that might take a moment to get your head around.

Lets look at some code so you can get a better understanding of what I’m talking about.


EventDispenser = Class.create();
EventDispenser.prototype = {
  initialize: function(list) {
    this.list = list;

    // Observe clicks on our list items     
    $$(this.list + " li").each(function(item) {
      Event.observe(item, 'click', this.showTagName.bindAsEventListener(this));
    }.bind(this));

    // Observe when a key on the keyboard is pressed. In the observer, we check for 
    // the tab key and alert a message if it is pressed.
    Event.observe(document, 'keypress', this.onKeyPress.bindAsEventListener(this));

    // Observe our fake live search box.  When a user types something into the box, 
    // the observer will take that value(-1) and update our search-results div with it.
    Event.observe('search', 'keypress', this.onSearch.bindAsEventListener(this));

    Event.observe(document, 'mousemove', this.onMouseMove.bindAsEventListener(this));
  },


  // Arbitrary functions to respond to events
  showTagName: function(event) {
    alert(Event.element(event).tagName);
  },


  onKeyPress: function(event) {
    var code = event.keyCode;
    if(code == Event.KEY_TAB) alert('Tab key was pressed');
  },

  onSearch: function(event) {
    Element.update('search-results', $F(Event.element(event)));
  },

  onMouseMove: function(event) {
    $('mouse').value = "X: " + Event.pointerX(event) + "px Y: " + Event.pointerY(event) + "px";
  }

}

Whoa! What’s going on here? Well, we’ve defined our a custom class EventDispenser. We’re going to be using this class to setup events for our document. Most of this code is a rewrite of the code we looked at earlier except this time, we are working from inside an object.

View a live example of our document »

Looking at the initialize method, we can really see how things are different now. Take a look at the code below:


    // Observe clicks on our list items     
    $$(this.list + " li").each(function(item) {
      Event.observe(item, 'click', this.showTagName.bindAsEventListener(this));
    }.bind(this));

We’ve got iterators, binding and all sorts of stuff going on. Lets break down what this chunk of code is doing.

First we are hunting for a collection of elements based on it’s css selector. This uses the new Prototype selector function $$. After we’ve found the list items we are dealing with we send those into an each iteration where we will add our observers.


Event.observe(item, 'click', this.showTagName.bindAsEventListener(this));

Now looking at the code above, you’ll notice the bindAsEventListener function. This takes the method before it showTagName and treats it as the method that will be triggered when, in this case, someone clicks one of our list items.

You’ll also notice we pass this as an argument to the bindAsEventListener function. This simply allows us to reference the object in context EventDispenser inside our function showTagName.

NOTE: If you prefer the jargon packed explanation of bindAsEventListener, see the Script.aculo.us wiki.

Moving on, you’ll see bind(this) attached to our iterator function. This really has nothing to do with events, it is only here to allow me to use this inside the iterator. If we didn’t use bind(this), I couldn’t reference the method showTagName inside the iterator.

Ok, so we’ll move on to looking at our methods that actually get called when an event occurs. Since we’ve been dealing with showTagName, lets look at it.


  showTagName: function(event) {
    alert(Event.element(event).tagName);
  }

As you can see, this function accepts one argument–the event. In order for us to get the element which fired the event we need to pass that argument to Event.element. Now we can manipulate it at will.

This covers the most confusing parts of our code. The text above is also relevant to the remaining parts of our code. If there is anything about this you don’t understand, feel free to say so.

Removing Event Listeners

This one threw me for a loop the first time I tried to use it. I tried something similar to what I did in the Event.observe call with the exception of using stopObserving, but nothing seemed to change. In other words, the code below does NOT work.


$$(this.list + " li").each(function(item) {
  Event.stopObserving(item, 'click', this.showTagName);
}.bind(this));

What’s the deal here? The reason this doesn’t work is because there is no pointer to the observer. This means that when we passed this.showTagName in the Event.observe method before hand, we passed it as an anonymous function. We can’t reference an anonymous function because it simply doesn’t have a pointer.

So how do we get the job done? All we need to do is give the observing function a pointer, or the jargon free version: Set a variable that points to this.showTagName. Ok, lets change our code a bit.


this.showTagObserver = this.showTagName.bindAsEventListener(this);

// Observe clicks on our list items     
$$(this.list + " li").each(function(item) {
  Event.observe(item, 'click', this.showTagObserver);
}.bind(this));

Now we can remove the event listeners from our list like this:


$$(this.list + " li").each(function(item) {
  Event.stopObserving(item, 'click', this.showTagObserver);
}.bind(this));

One final note on removing event listeners. If you have an instance where you want to simply remove all observes in one big swoop, you can use unloadCache.


Event.unloadCache();

Summing Up

That pretty much sums up events in Prototype. If you find any errors, please let me know. The great thing about writing articles such as this is that I also learn so much in the process and develop a deeper understanding of what I’m writing about. For instance, I found out the removing event listeners bit while I was writing this article. If you’ve got anything you’d like to contribute, feel free to chime in. Until next time, Happy Prototyping!

Related Reading

Discussion

  1. Tim Lucas Tim Lucas said on February 7th

    Thanks Justin for this much needed explanation.

  2. rick rick said on February 8th

    You always seem to write these articles after I spend some good time fumbling through them myself. Great article though…

  3. Jim Geurts Jim Geurts said on February 8th

    Can you explain the $$ syntax, please?

  4. Justin Palmer Justin Palmer said on February 8th

    Rick: I do that on purpose ;-).

    Jim: The $$ syntax is fairly straight forward. It works pretty much like it’s cousin $ except instead of passing it the id of an element, you pass a css selector and it returns an iterable, or as I like to call it a group/collection of stuff you can loop through.

    Assuming for a moment that you had a list with the id of articles and you wanted to get all of it’s li children, you’d do something like this:

    $$('#articles li')

    And if we wanted to work with those list elements, say we wanted to hide all of them we could do:

    $$('#articles li').each(Element.hide)

    I haven’t tested if it accepts attribute selectors like `input[type=text]` yet though. Thats one for another article.

  5. copongcopong copongcopong said on February 8th

    great article! The article’s goal is the same with Behaviour ’s goal.

    prototype’s $$ = Behaviour ’s document.getElementsBySelector = Dean Edwards’ cssQuery = JQuery ’s $

  6. no one no one said on February 8th

    what’s the point of using the alternate coloured lines as a background image if the text doesn’t line up properly? it makes it very tricky to read.

  7. Chuck Bradley Chuck Bradley said on February 9th

    Thanks for this tutorial! I really appreciate all the documentation that you and others provide.

    In the code segment under “Getting the coordinates of the mouse pointer,” there’s an error. The parenthesis after event should be closing rather than opening (I believe).

    ...function(event({$('mouse')...

    Should be:

    ...function(event){$('mouse')...
  8. Justin Palmer Justin Palmer said on February 9th

    Chuck: Thanks for pointing that out. Got it fixed.

  9. Jenna Fox Jenna Fox said on February 11th

    Yeah Justin, the background image lines problem that “no body” pointed out is pretty bad (in ubuntu linux firefox 1.5 official, even with MS fonts installed). I think it might be much nicer if you split the lines of code (perhaps client side in JS?) in to <div>’s or a similar element, then styled them with borders and background colours instead?

    Great article other than that! I was wondering some of the details about Event.observe, so thanks!

  10. Brian Brian said on February 13th

    Am I the only one that can’t get any of this to work in Internet Explorer 6.0(XP)?

  11. Scott Scott said on February 13th

    Is there a way to pass parameters to the function triggered by the action? This would be good because the examples shown here simply fire a function on “click”, but none of them show passing of variables to the receiving function.

    I’ve played with this a little but couldn’t get it to work.

    If you can pass variables, are you able to pass a reference to the class itself?

  12. Michael Michael said on February 15th
    Every time I try to observe keystrokes like in your example:
    Event.observe('search', 'keypress', function(event) { Element.update('search-results', $F(Event.element(event)));});
    the resulting output in the search-results box is one letter behind of the real content of the search box (in other words the output mirrors the content of the field just before the keypress that triggers the event! Any idea how to avoid this?
  13. Michael Michael said on February 15th

    Just for your interest: I “solved” my problem by adding a timeout of 1 millisecond to the function. This seems time enough for the browser to update its form-contents! ;-) (Firefox 1.5 on Linux) Nevertheless I would appreciate a cleaner solution!

  14. Rainer Rainer said on March 23rd

    Thanks a lot for your tutorial …

  15. choiz choiz said on April 19th

    test

  16. Ben Ben said on April 27th

    the example code at: http://encytemedia.com/demo/prototype/events/ is broken in firefox. You should probably fix this!

  17. Linkside Linkside said on June 15th

    I’ve coded a new method ‘stopObservingEx’ than remove listener more easily whithout any pointer. Ex: Event.stopObservingEx(Object, ‘focus’);

    // linkside new method : Remove observer more easy //
      stopObservingEx: function(element, name)
      {
          if (!Event.observers) return;
          for (var i = 0; i < Event.observers.length; i++){
              if(Event.observers[i][0] == element){
                  if(Event.observers[i][1] == name){
                      Event.stopObserving.apply(this, Event.observers[i]);
                      Event.observers[i] = null;
                  }
              }
          }
      },
    
    

    tlinkside@hotmail.com

  18. Linkside Linkside said on June 15th

    Without a bug :) new version:

    I’ve coded a new method ‘stopObservingEx’ than remove listener more easily whithout any pointer. Ex: Event.stopObservingEx(Object, ‘focus’);

    // linkside new method : Remove observer more easy //
      stopObservingEx: function(element, name)
      {
          if (!Event.observers) return;
          for (var i = 0; i < Event.observers.length; i++){
              if(Event.observers[i] && Event.observers[i][0] = = element){
                  alert(Event.observers[i]);
                  if(Event.observers[i][1] == name){
                      Event.stopObserving.apply(this, Event.observers[i]);
                      Event.observers[i] = null;
                  }
              }         
          }
      },
    
    tlinkside@hotmail.com
  19. Viktor Viktor said on July 19th
    I found that the order of the events is important. If you change the order of the click and load listeners, then this code won’t work.
    EventDispenser = Class.create();
    EventDispenser.prototype = {
        initialize: function() {
            Event.observe( 'column1', 'click', this.toggleColumn.bindAsEventListener(this) );
            Event.observe( 'column2', 'click', this.toggleColumn.bindAsEventListener(this) );
            Event.observe( 'footer', 'load', this.setFooterTop()  );
        },
    
        setFooterTop: function(){
            column1Top = Element.getHeight( 'column1' );
            column2Top = Element.getHeight( 'column2' );
            footerTop = Math.max(column1Top, column2Top) + 150;
            Element.setStyle( 'footer', {top: footerTop + 'px'});
        },
    
        toggleColumn: function(e){
            alert(e);
        }
    }
    

    The 2 columns are absolutely positionoed, and I would like to put the footer beneath them.

  20. Rekam Rekam said on July 30th

    Hi, thanks for this tuto ! I think that I’m now close to the solution, but my problem isn’t solved yet… Here it is :

    I’ve got a ‘click’ observer setted on an image (HTMLimgElement). When I want to stop the observation, it doesn’t work…

    this.img = document.createElement(‘img’); Event.observe(this.img, ‘click’, function(event){this.setOpen(event);}.bind(this), false);

    and when stopping : Event.stopObserving(this.img, ‘click’, function(event){this.setOpen(event)}.bind(this), false);

    But this doesn’t work… Any idea ? Thanks a lot!

  21. Kontaktanzeigen Kontaktanzeigen said on August 7th

    Thank for the nice tutorial on event selectors. No more inline onMouseOver !

  22. Blog Blog said on August 8th

    Nice Article and Site, Booked for Reference

  23. News News said on August 8th

    Looks very similar to Flashs Actionscript/

  24. Music Download Reviews Music Download Reviews said on September 2nd

    Interesting Article and Site, Booked for Reference.

  25. Maarten Manders Maarten Manders said on September 3rd

    I had a lot of trouble with stopObserving, too. Here’s a corrected version of Linkside’s script which had minor bugs in it.

    stopAllObserving: function(element, name) {
        var element = $(element);
          if (!Event.observers) return;
          for (var i = 0; i < Event.observers.length; i++) {
              if(Event.observers[i] && Event.observersi  element) {
                  if(Event.observersi  name) {
                      Event.stopObserving.apply(this, Event.observers[i]);
                      Event.observers[i] = null;
                  }
              }
          }
    },

Sorry, comments are closed for this article.