Improving Component Communication with the Dojo Toolkit

By on February 19, 2008 1:47 pm

Architect Header

I’m often asked how to keep communications clean between components when you’re dealing with widgets and objects coming from dynamic creation, markup creation, and widgets within widgets (within widgets!). Dojo provides several powerful techniques designed to make an application run faster, make it easier to maintain, and help the code look cleaner. We’ll explore some traditional component communication techniques, and how we might improve upon them using the Dojo Toolkit.

Global objects

In medium to large applications, global objects should be used sparingly. Keeping all of your code in the global namespace can cause conflicts if you merge in other code libraries or snippets, possibly clobbering functionality and causing hard to locate problems. Of course, you have to start with a global object or two, as dojo, dijit, and dojox are all global objects. But keep in mind global objects are slow to access.

JavaScript works from the inside out when seeking the variable you request, so if you are deep within your code, the JavaScript engine looks for the variable first within the function’s scope, then up the scope chain from there. Complex code and globals within loops can drastically reduce JavaScript speed. The general way to avoid these lookups is to make a local pointer to the global object and use that within your loop or widget: var local_app = window.app; so that it is only searched for once.

Dojo encourages proper coding habits by supplying the ability to create a namespace for your code. Using Dojo’s package system and dojo.registerModulePath, you can create not only an object where all of your code will live, but also point to a directory of your code. By using the following:

dojo.registerModulePath("acme", "../../acme");

…and creating a directory named acme on the same level with the dojotoolkit directory, you can now place all of your files in acme and all of your code could reside in the object acme.

Passing Objects

A classical Object Oriented technique is creating an object and passing the parent object into it. So we might have an addOnload that looks like this:

app = new acme.Main();
new acme.subWidget({parent:app}, node);

This is an effective technique and faster than global access, since the widget will pick up the global object app, assign it a reference named parent, and use it as a pointer. But you can start getting in trouble with deep hierarchies as they start looking something like: var foo = this.app.parent.parent.parent.bar. Worse, if object structures change during a refactor, it can be a nightmare to untangle the mess. But the main thing to watch out for are circular references. This can cause memory leaks if there is a DOM node involved. The following is a simple, yet real-world example of a circular reference:

function init() {
	var widget = {};
	widget.customProp = "foo";
	widget.element = document.getElementById("myDiv");
	widget.element.onclick = function() {
    		alert(widget.customProp);
  	};
}

The element node has a function attached to it. The function’s enclosure captures the local reference of the widget variable, and widget references element. We’ve created a circular reference that the browser cannot garbage collect, unless we add code to clean it up manually. And since it involves a JavaScript object, a method, and the DOM, it will leak memory.

dojo.connect

Connecting a function to a DOM node properly (and with cross browser compatibility) is much harder than it sounds, as many people have discovered. dojo.connect does a lot of heavy lifting, including preventing circular references between methods and DOM nodes and preventing memory leaks. And it makes your life easier by normalizing key strokes and event objects — not to mention ensuring you even get an event. Internet Explorer’s events are attached to the window and not returned, which is not only inconvenient, but messes up the context, pointing “this” at the window.

Using dojo.connect, the previous example can now be rewritten:

function init() {
	var widget = {};
	widget.customProp = "foo";
	widget.element = document.getElementById("myDiv");
		dojo.connect(widget. element, "onclick",  function( evt ) {
		alert( evt.target.customProp );
	});
}

Most commonly known as a way to connect functions to click events, it can actually connect to almost anything. For example, perhaps we don’t want to connect that onclick event until after the widget has finished its display method. We can connect to that method:

function init() {
	var widget = {};
	widget.customProp = "foo";
	widget.element = document.getElementById("myDiv");
	
	widget.display = function(){
		// - do display stuff - //
	}
	
	widget.myClick = function(evt){
		alert( evt.target.customProp );
	}
	
	var handle = dojo.connect(widget, "display", function(){
		dojo.connect(widget. element, "onclick", widget, "myClick");
		dojo.disconnect(handle);
	});
}

This illustrates dojo.connect’s ability to connect to functions and handle closures. Notice we also grabbed the handle from the display connection and disconnected it after it was used. This gives you a “connect once” ability (otherwise that will happen every time display fires). Now that we have our widget clean and leak-free, let’s wire it to an application.

Pub/Sub

Let’s say we want to click on widget.element and change the color of 50 “description” widgets in the content area. Let’s make the task even more difficult by saying there are another 50 “description” widgets we don’t want to change. And make it even more difficult by saying they were created dynamically and we don’t know their IDs or exactly where they may be in the DOM tree. Writing 100 lines of dojo.connects would not be the best solution here. What we can do is write two lines with Dojo’s pub/sub system, a reliable solution to JavaScript’s lack of any kind of delegation or watcher system. Let’s rewrite myClick to call a publish:

widget.myClick = function(evt){
	dojo.publish("/acme/change", [ {type:"apple", color:"blue"} ]);
}

Our widget will now broadcast the topic “/acme/change”, with the parameters {type:”apple”, color:”blue”}. Let’s now build a Dojo description widget that will listen for this topic:


dojo.declare("acme.DescriptionWidget", null
{
	templateString: '<div>${description}</div>',
	type:"", 
	description:"", 
	postCreate: function(){
		dojo.subscribe("/acme/change", this, "onChangeColor");
	},
	onChangeColor: function(options){
		if(options.type == this.type){
			this.domNode.style.color = options.color;
		}
	}
});


//now build the widgets with "server" data:
loadServerData = function(data){
	for(var i=0;i<100;i++){
		var w = new acme.DescriptionWidget({ 
			type:data[i].type, 
			description:data[i].desc 
		});
		contentNode.appendChild(w.domNode);
	}
}

We pass an object with the parameters type, and color, to target specific widgets or groups of widgets. Clicking on widget.element broadcasts a topic which our description widgets listen for. They check if the options apply to them and make changes accordingly. Other widgets could listen for the color-change topic as well, changing titles or borders for example.

The pub/sub system is an incredibly efficient way to communicate within a complex app. Most of my widgets start with a long list of subscriptions. In fact, my lists tend to get so long that I build them in a loop to optimize the code a bit. There aren't very many cons to this technique, but a few things to watch for are the order in which the widgets get their subscriptions (typically in the order in which they were registered), and that the publish was called before the subscription was registered. You also need to unsubscribe the topic when you destroy the widget or you will get unexpected results.

We've covered the most common Dojo features used to connect JavaScript objects safely and more efficiently. Using these techniques will help you build better and more maintainable applications.

Additional References

An Introduction to PubSub
Memory leak patterns in JavaScript
DOM Events, Memory Leaks, and You