This tutorial is for Dojo 1.7 and may be out of date.
Up to date tutorials are available.
Using dojo/behavior
It is frequently useful to isolate a page's "behavior" from its style and its content. The collection of event handlers that comprises that behavior layer can be succinctly defined and applied with dojo/behavior
.
The Problem
In this tutorial series, we've looked at using both dojo/on
and dojo/query
to set up event listeners on elements. Each technique works well, but there are challenges when you apply them to a larger problem.
As your DOM changes and you inject new content into the page, you need a way to set up the event listeners on new elements, while not doubling-up the event handling for ones that existed previously. This can be awkward.
At a more general level, while you might have a nice separation of concerns in your HTML and CSS, the behavior of your page (the binding of events to elements) is mixed up with other concerns in your javascript. Or, worse, inlined into your HTML code.
The Behavior Layer
The behavior layer describes a conceptual grouping of all the event binding that occurs during the lifetime of a page. It dates back to Ben Nolan's Behaviour.js library, and has implementations in many javascript libraries. When you remove this "layer" you have a nicely degraded, universally accessible page. With the layer applied you add runtime, javascript-driven interactions. A "behavior sheet" is analogous to a stylesheet and offers many of the same benefits of organization and maintainability. It collects together the mappings between elements, events and event-handling functions into a single object, which can be applied — and crucially — cleanly re-applied when the page content changes. This is a great way to implement unobtrusive scripting, but it goes further: by structuring the code to be organized around CSS selectors and event name/handler pairs, all the code entry points and event-level interactions on a page can be quickly scanned and understood.
Getting Started
Dojo's implementation of this concept is in the dojo/behavior
module.
Let's look at a simple example.
In the following demo, our page has a list of "products", each with its own button.
The desired behavior is that clicking on any button should increment the count in the summary.
We'll also add some style changes to highlight the button's clicked state.
First, the markup:
<h3>Product List</h3> <ul> <li> <h4>Product line 1</h4> <button class="buyButton demoBtn">Buy Me</button> </li> <li> <h4>Product line 2</h4> <button class="buyButton demoBtn">Buy Me</button> </li> <li> <h4>Product line 3</h4> <button class="buyButton demoBtn">Buy Me</button> </li> </ul> <div id="summary" class="summary">No items in cart</div>
The JavaScript bit looks like this:
// Require the behavior resource require(["dojo/dom", "dojo/dom-class", "dojo/behavior", "dojo/_base/xhr", "dojo/query", "dojo/topic", "dojo/_base/event", "dojo/NodeList-dom", "dojo/domReady!"], function(dom, domClass, behavior, xhr, query, connect, baseEvent) { // track the number of products "bought" var productCount = 0; // function to update rendering for the summary function onUpdate(){ dom.byId("summary").innerHTML = productCount + " items in cart"; } // function to handle click on 'buy' buttons function onPurchaseClick(evt){ productCount++; onUpdate(); } // a simple behavior 'sheet', which sets up event handlers on all elements // which match the '.buyButton' query var myBehavior = { ".buyButton" : { onclick: onPurchaseClick, onmousedown: function(evt){ domClass.add(evt.target, "buttonDown"); }, onmouseup: function(evt){ domClass.remove(evt.target, "buttonDown"); } } }; // register the behavior 'sheet' behavior.add(myBehavior); // apply all registered behaviors to the current document behavior.apply(); });View Demo
In the code listing above, the myBehavior
object is our "behavior sheet". A CSS selector is used as the key in the behavior definition, associated with one or more event-name: handler-function pairs.
In this simple example, we have defined onclick
, onmousedown
and onmouseup
event bindings for elements matching the ".buyButton
" selector.
The guts of this behavior should be very familiar; all that is new here is how we've composed the behavior object.
The final two lines are where we register and apply the behavior.
Notice that the definition of the behavior can be done before the DOM is ready and even before dojo/behavior
itself is loaded. Until we pass it to behavior.add
, it is simply a JavaScript object with no dependencies. This turns out to be useful, as we'll see later.
Behavior is not quite the same thing as event delegation.
Dojo's behavior implementation adds event listeners to each of the matched elements, rather than the event delegation approach that adds a single listener for each event type, higher up the DOM tree.
The dojox/NodeList-delegate module provides event delegation support for dojo/NodeList
(and therefore dojo/query
).
Re-applying Behavior
So far, we've not done anything we couldn't easily do with dojo/query
(although the behavior definition has a clarity that is useful all by itself.)
For our next demo, we'll dynamically add more content which we wish to have the same behavior:
We've added a little new markup - another list - with a link to load in more content:
<h3>Recommendations</h3> <ul id="recommendedList" class="productList"> <li><a class="recommendedLink" href="./recommendedItems.html">Get Recommendations</a></li> </ul>
We've extended the script with a function to load and extract out the content we need from a full HTML response:
function loadContentAsFragment(node, contentUrl){ // summary: // Fetch a page and extract out the list items return xhr.get({ url: contentUrl, load: function(html){ // extract out the list items html = html.substring(html.indexOf('<li')); html = html.substring(0, html.lastIndexOf('</li>')+5); node.innerHTML = html; } }); // return the deferred }
The loadContentAsFragment
function is a means of getting new content into the page. Notice that it returns the result of the dojo/xhr
call.
var myBehavior = { ".buyButton" : { onclick: onPurchaseClick, onmousedown: function(evt){ domClass.add(evt.target, "buttonDown"); }, onmouseup: function(evt){ domClass.remove(evt.target, "buttonDown"); } }, ".recommendedLink": { onclick: function(evt){ baseEvent.stop(evt); loadContentAsFragment( dom.byId("recommendedList"), // the target node evt.target.href // the url ).then(function(){ // when the new content is loaded in, // re-apply the behavior behavior.apply(); }) } } };
The "Get Recommendations" link in our markup was given a class of recommendedLink
.
The '.recommendedLink
' group in our behavior object defines a click handler for this kind of element - to inject the new content via the loadContentAsFragment
function.
We know this will change the DOM, so the behavior definition is the right context for re-applying behavior, via behavior.apply()
.
That leaves a clear separation of concerns:
- The markup has content and links. This will degrade gracefully for search bots and non-javascript enabled browsers.
- The library function
loadContentAsFragment
knows how to fetch content from a URL, and inject it into the node provided. It knows nothing about page behavior, or event handlers etc. - The behavior knows which events to wire up to which elements.
Behaviors as Modules
We can further highlight the separation of the behavior layer by defining it and loading it as its own module, using declare
and require
.
This reduces our page initialization code down to:
<script> // Get dojo so we can require the right path require(["dojo"], function(dojo) { // map the current directory as the path for code in the 'tutorial' namespace dojo.registerModulePath("tutorial", location.pathname.replace(/\/\w+\.html$/, "")); // Require the behavior resource require(["dojo/behavior", "tutorial/behavior", "dojo/domReady!"], function(behavior, tutorialBehavior) { // register the behavior 'sheet' behavior.add(tutorialBehavior); // apply all registered behaviors to the current document behavior.apply(); }); }); </script>
The module itself can be as simple as an assignment, or it can include its own related functions:
define(["dojo/dom", "dojo/dom-class", "dojo/behavior", "dojo/_base/xhr", "dojo/_base/event"], function(dom, domClass, behavior, xhr, baseEvent) { // track the number of products "bought" var productCount = 0; // function to update rendering for the summary function onUpdate(){ dom.byId("summary").innerHTML = productCount + " items in cart"; } // function to handle click on 'buy' buttons function onPurchaseClick(evt){ productCount++; onUpdate(); } function loadContentAsFragment(node, contentUrl){ // summary: // Fetch a page and extract out the list items return xhr.get({ url: contentUrl, load: function(html){ // extract out the list items html = html.substring(html.indexOf('<li')); html = html.substring(0, html.lastIndexOf('</li>')+5); node.innerHTML = html; } }); // return the deferred } // behavior 'sheet' return { ".buyButton" : { onclick: onPurchaseClick, onmousedown: function(evt){ domClass.add(evt.target, "buttonDown"); }, onmouseup: function(evt){ domClass.remove(evt.target, "buttonDown"); } }, ".recommendedLink": { onclick: function(evt){ baseEvent.stop(evt); loadContentAsFragment( dom.byId("recommendedList"), // the target node evt.target.href // the url ).then(function(){ // when the new content is loaded in, // re-apply the behavior behavior.apply(); }); } } }; });View Demo
The definition of the behavior has no module dependencies, nor does it need to wait until the DOM is ready. You can define different behavior modules for different pages. This code can be developed, versioned, iterated and even delivered and cached as its own entity. Or, it can be minified and packaged up with your other javascript code using Dojo's build tools.
The "found" Event
In addition to the usual DOM events, dojo/behavior
also supports a synthetic "found" event.
Handler functions are invoked once for each element matched by the selector/key.
This proves very useful for doing initialization. Here it is in action:
// snip for brevity. See demo/found.html // function to handle 'found' event for products function onProductFound(elm){ availableCount++; } // a simple behavior 'sheet', which sets up event handlers on all elements // which match the '.buyButton' query var myBehavior = { "button": { // bindings for all button elements found: function(elm){ console.log("button found: ", elm); }, onmousedown: function(evt){ domClass.add(evt.target, "buttonDown"); }, onmouseup: function(evt){ domClass.remove(evt.target, "buttonDown"); } }, ".buyButton": { // bindings for buyButtons specifically found: onProductFound, onclick: onPurchaseClick }, "#resetButton": { // wire up the reset button so it clears the products-purchased count onclick: function(evt) { productCount = 0; onUpdate(); } } };View Demo
In the above example, the found
pseudo-event for the .buyButton
group calls the onProductFound
function, to increment the count in a visual summary.
The onProductFound
function therefore is invoked once for each of our "Buy Me" buttons on the page.
The found
pseudo-event fires once for each element matched by a selector in the behavior. To demonstrate this, we also have a found
handler associated with a "button
" CSS selector.
This matches all <button> elements on the page and logs a message to the console. Even though .buyButton
and button
are overlapping sets, each of the behaviors is applied. In this example, our "Buy Me" buttons are 'found' twice.
Publish & Subscribe with dojo/behavior
The dojo/behavior
module has one more trick up its sleeve. To further facilitate a loosely-coupled component design, a behavior can be configured to publish events on a given topic.
Pub/sub is a common programming technique in which handler functions are associated with a topic or "channel".
In Dojo this is accomplished with dojo/subscribe
.
Any code can then publish events on this topic using dojo/publish
, and all registered handlers will fire.
Pub/sub is covered in our events tutorial.
In the context of dojo/behavior
, it looks like this:
topic.subscribe("/buyButton/clicked", function(evt){ // a default listener for click events on 'buy' buttons console.log("default handler for events on the /buyButton/click topic. isActive: ", isActive); }); var handles = {}, // where we'll store our subscribe handles isActive = false; // track active state of the form function toggleActive(){ if(isActive) { // set to inactive state // unsubscribe listeners query(".productList").addClass("inactive"); handles.activate.remove(); } else { // set to active state // start listening to the /buyButton/clicked topic handles.activate = topic.subscribe("/buyButton/clicked", purchaseProduct); query(".productList").removeClass("inactive"); } isActive = !isActive; } function onActiveClick(evt){ // update active state toggleActive(); evt.target.innerHTML = isActive ? "Deactivate" : "Activate"; } // a simple behavior 'sheet', which sets up event handlers on all elements // which match the '.buyButton' query var myBehavior = { ".buyButton" : { // publish an event on the '/buyButton/clicked' channel onclick: "/buyButton/clicked", onmousedown: function(evt){ domClass.add(evt.target, "buttonDown"); }, onmouseup: function(evt){ domClass.remove(evt.target, "buttonDown"); } }, "#activate": { // wire up the toggle button onclick: onActiveClick } };View Demo
The demo shows how you can easily broadcast events, and stop and start listening for events, all from a simple behavior sheet.
Caveats
There has to be a catch, right? What you can't easily do with dojo/behavior
is unregister any or all behaviors.
Under the hood, when you apply behavior, dojo/on
and dojo/topic
are used. The handles they produce that would facilitate removal are not collected or made available to you.
For many/most use cases, that does not present a problem, but for long-lived single-page-applications where you need finer-grained control over event registration and teardown, dojo/behavior
may not be suitable.
The sweet-spot for dojo/behavior
is that for which it was originally designed: unobtrusive scripting of content-focused pages.
Conclusion
Managing event handlers and page initialization are areas that can quickly produce spaghetti code.
The concept of the behavior layer and its implementation in dojo/behavior
can bring clear purpose to your code, allowing you to tease apart the mechanics of event handling from the functionality that should result from the event.
The behavior layer makes great "glue". By collecting all your event wiring together in a behavior sheet, all entry-points and the DOM structures and events they are associated with are clear and self-documenting.