Caridy, a leading and always-helpful contributor to the YUI mailing list, has been working in front of a PC since the nineties. For the past eight years he’s been a professional programmer developing LAMP applications for the University of Las Villas where he received his B.S. in Computer Science in 2003, and for several companies around the world. He left his university job in August to pursue an independent consulting career and to follow his passion for open source solutions and agile development.
For the last two years he’s been focused on JavaScript as a development platform. Early this year he decided to create an easy-to-adopt YUI extension called “Bubbling Library” as a side project.
There have been many influential articles about event-driven programming within the web browser, and developers are increasingly using this technique. But there is room to push the approach even further, and with it the capabilities of our web applications. In this article I’ll share my experiences in this space and show how my Bubbling Library, combined with YUI’s Custom Event capabilities, can create an unobtrusive behavioral layer suitable for powerful web applications.
In the beginning, behaviors were defined as inline attributes in the HTML layer. For example, we could assign a click handler inline by writing <span onclick="foo()">. But with the rise of unobtrusive JavaScript — and DOM Level 2 — this technique became deprecated. The new school promotes using the addListener method to attach behavior to DOM elements. I call this “simple handling.” Simple handling — widely used by the JavaScript developers — is a simplification of the bubbling technique: The target of the event is the same element that will catch the event.
In general, the DOM event model is based on two main concepts, “Event Capture” (Trickling) and “Event Delegation” (Bubbling). You can define where to catch the event, but experts like Douglas Crockford caution you against using the trickling method and instead let the browser reach the event target. One reason for this is that it will be tough to combine trickling with the simple handling technique because the event will be caught before reaching a certain target. Another reason is that Internet Explorer provides incomplete support for the DOM-standard event handling during the capture or “trickling” phase.
The Simple Handling process will be run exactly after the trickling process and before the bubbling process.
In the event delegation, or bubbling model, the propagation process will continue (bubbling) upward until the event is canceled or reaches the document’s root (whichever comes first). You can intercept the event at any node on its path up the DOM by adding event listeners. (It is “simple handling” if the event listener is attached to the event target itself.) These listeners watch for specific events, for example click or mouseover. They know which method or function to call and they know which element was the target of the event. If desired, they can stop the event from continuing all the way up to the root. Using DOM level 2′s addListener method, you can add as many listeners as you like and have them wait for as many different events and call as many methods as you like.
The Yahoo! User Interface Library (YUI) implements a Custom Event Object. This pattern allows you to define events unique to your application, subscribe listener methods to them, and fire the events whenever you want. These “Interesting Moments” can immediately notify an unlimited number of components, controls, and widgets. The trigger of these interesting moments can be an event within the user interface, or a direct call fired by logic in your application.
Imagine a HTML page that has a dynamic content area that is updated by an AJAX method (using, for example, the YUI Connection Manager). Imagine that other components in the page need to know when new content arrives (so they can take some action). There are two ways to implement this:
var Foo = function () {
var obj = {};
// private stuff
var callback = {
success: function (o) {
// content substitution here...
// ...
// notification to every component of the new content's arrival...
obj.onArrive.fire();
},
failure: funciton (o) {}
}
handle = null;
// public vars
obj.onArrive = new YAHOO.util.CustomEvent('onArrive');
// public methods
obj.fetch = function (uri) {
handle = YAHOO.util.Connect.asyncRequest('GET', uri, callback);
};
return obj;
}();
Foo.onArrive.subscribe ( YAHOO.example.SnapShot );
Foo.onArrive.subscribe ( YAHOO.example.FormValidation );
The second approach, based on Custom Events, is preferable because it follows Modular Design principles, reducing brittleness and maintenance.
Another useful feature of Custom Events involves scope. During the creation of the object you can specify which object will be used as the default scope during the execution chain. You can also set a scope for each subscriber: each component can subscribe to other’s behaviors while keeping the execution within the component’s own scope. This is illustrated in the code below:
var navigate = new YAHOO.util.CustomEvent('navigate', navigateGlobalScope);
var onNavigate = function(e){
var t=(e?YAHOO.util.getTarget(e):null);
navigate.fire(e, {action: 'navigate', target: t, decrepitate: false});
};
navigate.subscribe ( YAHOO.example.myComponent.myBehavior, YAHOO.example.myComponent, true );
navigate.subscribe ( YAHOO.example.myOtherComponent.myOtherBehavior );
In this case the myBehavior method is executed under the scope of the component (myComponent), but in the other subscriber the scope of the execution will be the navigateGlobalScope object. By default the navigateGlobalScope param is the window object.
Another possible issue in the execution chain is event propagation; you can’t stop the event’s bubbling after certain subscribers execute. To solve this problem you can define a custom event’s scope value, and a condition based on the subscriber execution using this value.
How to deal with propagation in the Custom Event technique:
// preparing the Subscriber
var myGlobalBahavior = function (layer, args) {
// verifying if the event was already adopted, and checking if the target is available
if (!args[1].decrepitate && YAHOO.lang.isObject(args[1].target)) {
// Adopting the event and doing your stuff here
// ...
// Stopping the event's bubbling & preventing the default behavior (window) for this event
YAHOO.util.Event.stop(args[0]);
// Reclaiming the event and stopping the propagation
args[1].decrepitate = true;
}
};
// preparing the Custom Event
var navigate = new YAHOO.util.CustomEvent('navigate');
var onNavigate = function(e){
var t=(e?YAHOO.util.getTarget(e):null); // getting the event target
// starting the execution and defining the custom event's scope values
navigate.fire(e, {action: 'navigate', target: t, decrepitate: false});
};
navigate.subscribe ( myGlobalBahavior );
In this example we use a scope variable called “decrepitate” to track the status of the custom event execution chain. If the value is true, the event has already been consumed by one of the subscribers.
With event delegation you attach event listeners at high DOM-tree levels (closer to the root node). These listeners catch an event as it bubbles up the DOM during the bubble phase from a child node up through its parents. This allows us to have fewer event listeners while still processing events before the browser fires the default behavior for that event.
The corresponding code for catching events at a high DOM level is shown below:
YAHOO.util.Event.addListener(window, "resize", function (e) {} );
YAHOO.util.Event.addListener(document.body, "click", function (e) {} );
YAHOO.util.Event.addListener(document.body, (isOpera?"mousedown":"contextmenu"), function (e) {});
YAHOO.util.Event.addListener(document.body, "mouseover", function (e) {} );
YAHOO.util.Event.addListener(document.body, "mouseout", function (e) {} );
// For "document.body" listeners you must wait until the DOM structure is ready (onDOMReady).
After we apply these listeners we can catch all applicable events and get their target — unless an event handler attached below our listener purposely stops the propagation process, of course. By doing this, we have created a behaviors “layer” because all the events will be listened for and caught at the same high level (close to or at the root). With this in place, our challenge is to manage all the events and bind certain targets (DOM elements) with certain behaviors (JavaScript functions).
The trick is to change the way we understand the connection between target and event. Usually you need to attach a listener (using, say, YAHOO.util.Event.addListener) to fire a certain behavior on a certain target. This approach is unfortunately overly DOM-centric: You need to wait until a DOM element is available to attach the listener. Before looking at a superior alternative, let’s look at a few additional disadvantages:
There is a nearly opposite approach: when an event is fired all available components are queried and action is taken if the event target corresponds. In this case, every component hangs its behaviors to the corresponding behavior layer (click, mouseover, mouseout, keypress, etc). This is a less-brittle approach because whether or not the component is available your application will function without error. (Note: priority, as mentioned above, is still important.) The disadvantages of this method are:
var navigate = new YAHOO.util.CustomEvent('navigate');
var onNavigate = function(e){
var t=(e?YAHOO.util.getTarget(e):null);
navigate.fire(e, {action: 'navigate', target: t, decrepitate: false});
};
YAHOO.util.Event.addListener(document.body, "click", onNavigate);
The next challenge is identifying the available behaviors for a certain event. The process is simple: after the creation of the behaviors layers — and right after the subscribers are ready — every event will reach its corresponding behavior layer depending on the event type (click, mouseover, etc) carrying its target reference. Using the event’s type (for example, click) and target (the DOM element), you can query every behavior. If a behavior accepts the event it can flag it (but not stop it) and notify subsequence behaviors that the event has already been “consumed.” (You can ignore this flag, of course.)
There are various ways to identify the target:
Each behavior will analyze each event based on:
click, mouseover, mouseout, etc.)tagName of the target, depending on the element typeclassName of the target, verifying if the target or an ancestor have a certain className attachedUsing my bubbling library (free, BSD license), a global behavior would look like this:
// If the event's target has a certain className ('actionMyGlobalBehavior'), this behavior will adopt the event
YAHOO.CMS.Bubble.addDefaultAction('actionMyGlobalBehavior',
function (layer, args) {
// Arguments:
// args[1].decrepitate - (Boolean) "True" If the event was already adopted
if (!args[1].decrepitate) {
// args[0] - (Event object)
// args[1].target - (DOM reference) Target element
// args[1].anchor - (DOM reference) If the target was an anchor
// args[1].button - (YUI button reference) If the target was a YUI button
// args[1].input - (DOM reference) If the target was an input
// ---------------
// your stuff here
//----------------
// consuming the event, and stopping the propagation
return true; // is equivalent to: args[1].decrepitate = true; args[1].stop = true;
}
}
);
As you may have noticed, it’s very simple to reuse components in different environments: Just include the component in the execution session (or load onDemand) and the component will attach its behaviors to the corresponding layers. If a class of DOM elements is available in a certain moment, the event will be caught and passed to a certain component. In this way you can have a library of components that may be used in different applications in a simple way without needing to modify the component’s behaviors.
Christian Heilmann mentioned the biggest plus of this approach in his January 2007 YUI Blog article Event-Driven Web Application Design.
…[It's good because you can] cut the big application down into manageable chunks and components and you can plan the detailed usability, information architecture and accessibility for each component separately. This allows you to develop in parallel with the design or information architecture team and results in reusable components for other applications.
addListener), and you’d like to change the element to a YUI Button Control.There are many additional examples you can explore.
September 14, 2007 at 12:32 pm
[...] The Bubbling Technique & Custom Event, YUI’s Secret Weapon by Caridy Patiño Mayea » Yahoo! U… In this article I’ll share my experiences in this space and show how my Bubbling Library, combined with YUI’s Custom Event capabilities, can create an unobtrusive behavioral layer suitable for powerful web applications. (tags: events yui javascript dom bubbling library article tutorial technique) [...]
September 14, 2007 at 3:40 pm
[...] The Bubbling Technique & Custom Event, YUI’s Secret Weapon by Caridy Patiño Mayea » Yahoo! U… (tags: article dom javascript library technique tutorial event web development) [...]
September 14, 2007 at 5:06 pm
[...] Bubbling Library posted by admin at 6:06 pm [...]
September 15, 2007 at 12:19 am
[...] The Bubbling Technique & Custom Event, YUI’s Secret Weapon by Caridy Patiño Mayea (Yahoo! Use… I’ve said it before, and I’ll say it again: I’m a fan of the YUI. This is another excellent article explaining the intracies of the custom event model. This model makes it a breeze to connect multiple components (even unaware of each other) on one page. (tags: webdev javascript yui tutorial events web2.0 ajax) [...]
October 25, 2007 at 7:37 am
[...] http://yuiblog.com/blog/2007/09/13/bubbling-library-by-caridy/ [...]
April 22, 2008 at 11:29 am
[...] For the last two years he’s been focused on JavaScript as a development platform. Early last year he decided to create an easy-to-adopt YUI extension called “Bubbling Library” as a side project; you can read his YUIBlog introduction to the Bubbling Library here. [...]
November 3, 2008 at 1:03 pm
[...] For the last two years he’s been focused on JavaScript as a development platform. Early last year he decided to create an easy-to-adopt YUI extension called “Bubbling Library” as a side project; you can read his YUIBlog introduction to the Bubbling Library here. [...]
November 16, 2008 at 8:23 pm
[...] YUI blog for a more detailed reading on YUI custom events and event [...]