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.

  • Difficulty: Beginner
  • Dojo Version: 1.7

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:

View Demo

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();
		}
	}
};

Buttons

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.

Resources

Error in the tutorial? Can’t find what you are looking for? Let us know!