Javascript: Event broadcasting/listening

Feb 2, 2006 by Andre

A key advantage of OO programming is reuse. The more broadly useful an object is, the more likely it is you will need to "wire it in" to your page (or other objects) in unanticipated ways. But, if you want the object to trigger event X when Y happens, how do you do it without changing the actual methods in your object?

To connect up your object up to other objects without violating its integrity, you need event broadcasting/listening.

To make this concrete, imagine a simple object representing a tri-state checkbox. HTML checkboxes are two-state (either checked or unchecked), and you need one with three states (checked, unchecked, and grayed out). You will program this as an image which responds to onClick events by cycling the image source through a series of three graphics. So far so good?

What do you want to invoke when the checkbox object changes state? If you're programming it in a non-reusable way, you can hard-code the action directly into the onclick event: if (state==checked) do X; if (state==grayed_out) do y; etc. If you're programming it as a reusable component, you need a more flexible solution.

First, in psuedo-code, for the onClick event:

// PSUEDO CODE!
method onClick() {
  if (state == unchecked) {
    update graphic src to "checked";
    broadcast "checked" event; 
  } else if (state == checked) {
    update graphic src to "grayed_out";
    broadcast "grayed_out" event; 
  } else if (state == grayed_out) {
    update graphic src to "unchecked";
    broadcast "unchecked" event; 
  }
}

Now, your page just needs a way to listen for the events, and you're set -- you can have anything on the page react to an event in the checkbox instance, without changing code inside the checkbox class itself.

How to do it

We're going to create an Observer object, which takes care of both event management (adding and removing events which can be broadcast) and notification (registering functions to be called in the case of a specific event).

/*
Event handler, intended to be used in composition with other objects
*/
function Observer () {
  this.add=add;
  this.remove=remove;
  this.notify=notify;

  this.oObservers = new Object();

  function add (sEvent, fObserver) {
    // must be one of the event types already defined in observers
    if (this.oObservers[sEvent] != null) {
      this.oObservers[sEvent].add(fObserver);
    } else {
      alert("attempted to add an observer for an event which doesn't exist. "+
       "Event Name is "+sEvent);
    }
  }
  
  function remove (sEvent, fObserver) {
    // must be one of the event types already defined in observers
    if (this.oObservers[sEvent] != null) {
      this.oObservers[sEvent].remove(fObserver);
    } else {
      alert("attempted to remove an observer for an event which doesn't exist. "+
        "Event Name is "+sEvent);
    }
  }
  
  function notify(sEvent,oArgs) { 
    if (this.oObservers[sEvent] != null) {
      var aTemp = this.oObservers[sEvent];        
      for(var i = 0; i < aTemp.length; i++) {
        aTemp[i](oArgs);
      }
    } else {
      alert("attempted to invoke an event which doesn't exist. "+
        "Event Name is "+sEvent);
    }
  }
  
  //initialization. If there are any arguments passed, 
  // add them and the associated arrays
  if (arguments.length >0) {
    for (i = 0; i< arguments.length; i ++) {
      this.oObservers[arguments[i]] = new Array();
    }
  }
}

Now, the tri-state checkbox can hold a reference to an instance of Observer, register the necessary events, and broadcast as appropriate. For this class, there is only one event (clicked), but you could have as many events as you want -- just separate them in a comma-delimited list.

// in the constructor for the checkbox class:
this.observers = new Observer();
this.observers.oObservers = {
	"clicked": new Array()
};

. . . .

// the "click" event for the checkbox 
// Pseudo-code except for the Observer.notify() part
onClick() {
  if (state == unchecked) {
    update graphic src to "checked";
    this.observers.notify("clicked",{"sNewState":"checked"});	
  } else if (state == checked) {
    update graphic src to "grayed_out";
    this.observers.notify("clicked",{"sNewState":"grayed_out"});	
  } else if (state == grayed_out) {
		update graphic src to "unchecked";
    this.observers.notify("clicked",{"sNewState":"unchecked"});	
  }
}

This code broadcasts the "clicked" event for each of the three states the checkbox can be in. The first argument to the observer.notify() method is the event name -- recall that for this example, only the "clicked event is defined.

The second argument to the observer.notify() method is a set of key-value pairs, allowing you to pass whatever information you want to the listener. The listener can use these values or discard them, but it has to know the key to access them. In this case we pass only one key (sNewState), but we could pass as many as we like in a comma-delimited list inside the {}'s.

Attaching listeners to events

So far, we've prepared an object for event broadcasting. All that's left to do set up listeners. Attaching listeners to the events the object broadcasts will let us utilize the class in unforeseen ways, without changing the internals of the class.

// assuming you have an instance of checkbox called oCheckBox:
oCheckBox.observers.add("clicked", 
  function(oArgs) {
    alert("You clicked it! New state is: "+oArgs.sNewState);
});		

The "function(oArgs)" is the event handler. We associated it to an instance of the CheckBox class by calling oCheckbox.observers.add(). We're specifying the "clicked" event in the first argument to the observers.add() method.

oArgs is the object we passed in the observers.notify() method. Recall that we specified just one key, sNewState. sNewState is used in the event handler alert().

Of course, in actual usage the object would be more complex, and it would broadcast more events. Handlers tend to stay relatively simply, however -- their main purpose is "wiring together" different objects, so an event in one triggers an action in another which is appropriate for the current page.

A final note: you can add as many event handlers as you like for any event on any object! Just call [object].observers.add() as many times as needed -- the handlers will be executed in the order you add them!

Event broadcasting/listening can add a huge about of flexibility to your Javascript OO programming, and allow you to utilize classes in diverse environments without changing their internals.

TrackBack URL:

Listed below are links to weblogs that reference Javascript: Event broadcasting/listening:

» OOP in Javascript examples from Record as I am
Andres at Web 2.0 Technologies has a very informative post about how to use object oriented techniques to implement a robust event broadcasting and listening in javascript.  Follow his advise an dmake your code more reusable. What do you want... [Read More]

Comments

1

wm on Dec 10

Hi there!
first of all thanks for this useful info,
so as far as I can understand if I've an onclick event and don't want any interference until the onclick functions are finished to execute (ie the users trigs other events before my onclick event did finish is job)
I've to use event broadcasting/listening, no?
sorry if maybe my question is silly... but some clarification for my mind is needed to make one step ahead ;)

2

aidrian o'connor on Jun 29

I *think* there may be a mistake in the Observer class code:

...
// must be one of the event types already defined in observers
if (this.oObservers[sEvent] != null) {
this.oObservers[sEvent].add(fObserver);
} else {
...

> this.oObservers[sEvent].add(fObserver);
should be
> this.oObservers[sEvent].push(fObserver);

this.oObservers[sEvent] is an array, and has no 'add' method defined. Pushing to the array seems to work, though I'm not positive that is what you intended.

Thanks for the good tutorial.

Post a comment

 
This is so filters can reject the spam-bots. Thanks!