Conquer event-driven design using dojo.connect

Explore how dojo.connect and pub/sub make web applications more maintainable

This article explains how event-driven design is loosely coupled and fits neatly into a browser-based application and Dojo's object-oriented framework. The result is a tool that helps maintain modularity in code. The modules are then glued together using dojo.connect, a powerful function that you can use not only for Document Object Model (DOM) events, but also for custom application events. Finally, the article compares dojo.connect and pub/sub, including their different degrees of loose coupling.

Share:

Mike Wilcox, Director of Technology, BetterVideo

Author photoMike Wilcox is Director of Technology for BetterVideo, a fast-growing startup in Frisco, Texas; he is in charge of front-end engineering and online video services. Mike is a regular speaker on Ajax and other web technologies, and has spoken at the 2009 Rich Web Experience, the 2009 Dallas TechFest, and many other conferences. His open source work is on display in the Dojo Toolkit, where, as a committer, he's implemented many of the multimedia technologies; these include the Multi-file Uploader, the audio and video components, and the vector-based DojoX Drawing. You can reach Mike at mike@mikewilcox.net.



25 January 2011

Also available in Chinese Japanese

What is event-driven design?

Develop skills on this topic

This content is part of a progressive knowledge path for advancing your skills. See Get started with Dojo development

Event-driven design is a programming paradigm in which the flow of the program is determined by events—rather than a series of functions—executing in a predetermined or linear flow. Instead of the execution cycle of one function finishing and then calling another, or that function registering to a cycle and waiting for a callback to be fired, the module "listens" for the completion of the cycle; this is signaled by an event.

Advantages

Web pages are essentially user interfaces triggered by mouse actions or keyboard inputs. Because event-driven design is already built into the browser, it's natural to extend it to web applications. The JavaScript language uses event listeners to detect user actions and resource loading. You can use this same pattern to listen to custom events or actions within your code. The advantage of this design is that JavaScript modules (or classes) can be more effectively self-contained as black boxes; these are loosely coupled objects that can be more easily added, removed, replaced, and maintained with little or no disruption to the overall program code.

Disadvantages

Cross-browser issues make it difficult to implement an event-listening system with JavaScript code. Microsoft® Windows® Internet Explorer uses a nonstandard and improperly implemented event listener that in most cases can't be disconnected, doesn't pass the event as an argument, and doesn't provide a clear way to block the events. While all other browsers use the standardized addListener system, the events generated tend to be different not only from browser to browser, but also within the same browser on different operating systems.

What is loose coupling?

Coupling refers to the degree to which one class knows about another. This is not to say that the classes can't know of each other's property or methods, but instead addresses their knowledge and reliance upon the class itself. When you have two classes that are strongly coupled, you can't make changes to one without upsetting the implementation of the other.

An example of strong coupling might be that Class A loads some data, then calls dataLoaded() on Class B. Class B then gets the Foo property from Class A. This system can easily break if Class B is removed, the Foo property is renamed, or the function signature of dataLoaded is changed.

An example of loose coupling might be that Class A loads data and then fires the onDataLoaded event. Class B uses an event listener on onDataLoaded, and, when it fires, reads the event object to get the Foo property.

Coupling is relative; that is to say, while the second example is considered loose, Class B still needs to know about Class A to connect to one of its events. Global event systems are used to achieve a higher level of loose coupling.

What is dojo.connect?

dojo.connect allows the source of events to be either a regular JavaScript function or a DOM event. It provides a uniform interface for listening to all the types of events that an application is likely to encounter though a single, unified interface. This function is valuable for developers who have tried to create code that connects, disconnects, and normalizes DOM events.

In Listing 1, the code connects to the myButtonclick event. When the click event fires, the anonymous function is invoked.

Listing 1. Simple dojo.connect example
dojo.connect(dojo.byId("myButton"), "click", function(evt){
	console.log("The button was clicked: ", evt.target.id);
});

Advantages

All libraries have a function that connects to DOM events. What sets dojo.connect apart is that it also includes the ability to connect to custom events or routine functions, as shown in Listing 2.

Listing 2. A dojo.connect to function example
myObject = {
	method: function(){
		//
	}
};
dojo.connect("myObject", "method", function(evt){
	console.log("myObject.method fired");
});

Disadvantages

Since dojo.connect was intended to handle everything, sometimes baffling errors that are difficult to track sneak in. In addition, the code to make both multiple connections and a connect once function can get touchy. Finally, by nature of the fact that you have to connect to a specific object to listen to it, you have to be aware of that object in the first place. This can make for tricky code, which developers want to avoid.

Problems that dojo.connect solves

As shown in Listing 3, it is not difficult to write your own function callbacks.

Listing 3. Custom callback example
myObject = {
	method: function(data, callback){
		// do something asynchronous here...
		setTimeout(function(){
			var value = doSomething(data);
			callback(value);
		});
	}
};
myObject.method(42, function(result){
	console.log("the result: ", result);
})

But callbacks can be limiting. A second call might step on the first. You might find you have several areas in your application that need a callback, each of which needs new code. This requires much repetition, which is hardly efficient.

Example project

This section provides a simple yet tightly coupled application to demonstrate an event-driven design, and then shows how to decouple the classes with dojo.connect. Only the relevant parts of the code are shown within the article. For the complete code, see the Download section.

The sample application has two drop-down selects. The first contains American states, and the second lists a few cities from the selected state. The selection of one state from the first list alters the list of city options in the second drop-down, as shown in Figure 1.

Figure 1. Sample application with state and city drop-down selects
Two Selects, the first changes the second

Listing 4 shows the base Hypertext Markup Language (HTML) code that is used for all the examples. It loads the necessary Dojo files for a dijit.form.Select, including the new Claro style sheet. Two custom widgets are instantiated in markup.

Listing 4. Base HTML code for examples
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 5//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Tightly Coupled Example</title>
<link href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/dijit.css"
	rel="stylesheet" />
<link href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css"
	rel="stylesheet" />
<script src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js"></script>
<script>
	dojo.require("dijit.form.Select");
</script>
<script>
	dojo.ready(function(){
		// code will go here
		dojo.parser.parse();
	});
</script>

</head>
<body class="claro">
    <h1>Tightly Coupled Example</h1>
	<div dojoType="StateSelect" id="stateSelect"></div>
	<div dojoType="CitySelect" id="citySelect"></div>
</body>
</html>

Tightly coupled application

What is a CDN?

A CDN is a content delivery network. Google and AOL provide Dojo code and other Asynchronous JavaScript + XML (Ajax) libraries for use in your web pages.

The code replaces the comment code will go here and is placed inside a dojo.ready that fires when all the code is loaded from the CDN. When the DOM is ready to access, it is completed with dojo.parser.parse(), which instantiates the markup. Two custom widgets extend dijit.form.Select: StateSelect is used in the first example and does all the heavy lifting, and CitySelect is used in the second example but doesn't do anything special yet. Refer to Listing 5.

Listing 5. Tightly coupled code
dojo.declare("StateSelect", [dijit.form.Select], {
	postCreate: function(){
		this.inherited(arguments);
		dojo.xhrGet({
			url:"geography.json",
			handleAs:"json",
			load: dojo.hitch(this, function(data){
				this.data = data;
				this.addOption(this.data.items);
			})
		});
	},
	onChange: function(value){
		for(var i = 0; i < this.data.items.length; i++){
			if(this.data.items[i].value == value){
				var menu = dijit.byId("citySelect").getOptions();
				dijit.byId("citySelect").removeOption(menu);
				var newMenu = this.data.items[i].children;
				dijit.byId("citySelect").addOption(newMenu);
			}
		}
	}
});

dojo.declare("CitySelect", [dijit.form.Select], {

});

This example is tightly coupled. When StateSelect is created, the Dijit system fires postCreate. At that point, the geography.json file is loaded with dojo.xhrGet, and the drop-down menu is created within that callback mechanism (load).

The Dijit system then fires onChange when a new menu item is selected from the drop-down, passing the value of that option. The code loops through the data until the corresponding data item—which contains the child items for CitySelect—is found. The CitySelect is found using dijit.byId; its current menu is removed and replaced by a new one.

What makes this code tightly coupled? A desirable approach to creating maintainable code is to reduce its side effects; you can estimate the number of side effects by counting the number of methods and comparing that to the number of actions taken. This example demonstrates two methods (postCreate and onChange) and four actions:

  1. Initialize widget (postCreate)
  2. Handle selection (onChange)
  3. Load data
  4. Handle data load

Another reason why this code is tightly coupled is that StateSelect takes liberties with CitySelect's methods. Though these methods are all public and intended to be accessed externally, this access can be removed or reduced to achieve loose coupling. The goal is to create a black box—an object that is self-sufficient. Semantics also make code more maintainable: loading data happens in a loadData method, and handling the loaded data happens in an onDataLoaded method. This application is quite small. If the data were 100 times larger and used this style, finding where the data is loaded and parsed would be quite difficult. And if you were to encounter a problem with CitySelect, you might be surprised to find that there is no code—at least not in this child class.

Loosely coupled application

Listing 6 shows the reworked application that uses the proper semantic methods and results in fewer side effects.

Listing 6. Loosely coupled code
dojo.declare("StateSelect", [dijit.form.Select], {

	postCreate: function(){
		this.inherited(arguments);
		this.loadData();
	},
	loadData: function(){
		dojo.xhrGet({
			url:"geography.json",
			handleAs:"json",
			load: dojo.hitch(this, "onDataLoaded")
		});
	},
	onDataLoaded: function(data){
		this.data = data;
		this.addOption(this.data.items);
	},
	onChange: function(value){
		// stub to connect to
	}
});

dojo.declare("CitySelect", [dijit.form.Select], {
	postCreate: function(){
		this.inherited(arguments);
		this.stateSelect = dijit.byId("stateSelect");
		dojo.connect(this.stateSelect, "onChange", this, "onStateChange");
		dojo.connect(this.stateSelect, "onDataLoaded", this, "onDataLoaded");
	},
	onDataLoaded: function(data){
		this.data = data;
		this.dataMap = {};
		for(var i = 0; i < this.data.items.length; i++){
			this.dataMap[this.data.items[i].value] = this.data.items[i];
		}
	},
	onStateChange: function(value){
		this.removeOption(this.getOptions());
		var item = this.dataMap[value];
		var menu = item.children;
		this.addOption(menu);
	}
});

By separating the functionality, the CitySelect now has methods it can attach to, using dojo.connect. CitySelect can now take care of itself and change its menu options when the onStateChange event is fired. CitySelect now maintains its own reference to the data as well.

Hash maps for performance

You might notice the created dataMap property used in Listing 6. A good performance tip is to make a hash map of your data so you can find your data items almost instantly instead of looping through arrays.

If your initial reaction to the application rewrite was surprise at how much code is included, remember this: in a small application, the difference in size is obvious, but as the application grows, you'll find much less code repetition. This means less code. And even if the actual size of the code is slightly greater, the tradeoff for better maintenance is worth the cost.

Extending dojo.connect

The reason that you can't just pass a node ID as the first argument with dojo.connect is that you can pass as few as two arguments to dojo.connect as strings representing two global functions, as shown in Listing 7.

Listing 7. Connecting two global functions
foo = function(){}
bar = function(){
	console.log("bar");
}
dojo.connect("foo","bar");
foo(); // outouts "bar"

With this functionality, you can't assume that if the first argument is a string, then it is a DOM ID simply because the traditional assumption is that of a function name. But the beauty of the JavaScript language is that you are not locked in to any of Dojo's default functionality.

Accept a DOM ID

Listing 8 shows how simple it is to overwrite dojo.connect by assigning it to another name, adding your own conditionals, and then passing all the arguments to the newly named dojo._extconnect.

Listing 8. Adding ID capability
dojo._extconnect = dojo.connect;

dojo.connect = function(source, event, object, method, once){
	source = typeof(source)=="string" ? dojo.byId(source) : source;
	if(!source) throw new Error("Bad source passed to dojo.connect:", source);
	dojo._extconnect.apply(dojo, arguments);
}

myObject = {
	onMouseAction: function(evt){
		console.log("mouse action:", evt.type);
	}
};
dojo.ready(function(){
	dojo.connect("btn", "click", myObject, "onMouseAction");
});

// HTML:
<button id="btn">Mouse Actions</button>

Now you can pass "btn" instead of dojo.byId("btn"). Notice that I added a line to check for a source so that it gives an error message if it's not found. In my opinion, this can be much more useful than having Dojo assume that "null equals window."

Of course, adding this line is optional. This extension and the subsequent extensions inhibit some of the default functionality. In this case, you can't pass two strings as global functions. I personally find that all these extensions outweigh the seldom-used default behaviors.

Multiple connects

You might have come across code that uses some form of connecting to DOM events repeatedly, as shown in Listing 9.

Listing 9. Gratuitous use of connect
dojo.connect(dojo.byId("btn"), "click", myObject, "onMouseAction");
dojo.connect(dojo.byId("btn"), "mouseover", myObject, "onMouseAction");
dojo.connect(dojo.byId("btn"), "mousedown", myObject, "onMouseAction");
dojo.connect(dojo.byId("btn"), "mouseout", myObject, "onMouseAction");

Since connect is already modified, you can modify it further to reduce this type of repetitive pattern. Test whether the second argument is a string, as usual, or an array to connect the source to multiple times, as shown in Listing 10.

Listing 10. Multiple connect code
dojo._extconnect = dojo.connect;

dojo.connect = function(source, event, object, method, once){
	source = typeof(source)=="string" ? dojo.byId(source) : source;
	if(!source) throw new Error("Bad source passed to dojo.connect:", source);
	if(dojo.isArray(event)){
		dojo.forEach(event, function(e){
			dojo.connect(source, e, object, method, once);
		});
	}else{
		dojo._extconnect.apply(dojo, arguments);
	}
}

myObject = {
	onMouseAction: function(evt){
		console.log("mouse action:", evt.type);
	}
};

dojo.ready(function(){
    dojo.connect("btn", ["click", "mousedown", "mouseover"], myObject, "onMouseAction");
});

At this point, all the events in the second argument have been passed to the method. This is convenient if you handle a lot of connections. (Notice that this example doesn't allow for the ability to disconnect the events. You can do that by extending dojo.disconnect and checking the type of argument.)

Connect once

One popular custom extension is to connect to an event and then disconnect it after it fires once. This is useful for custom events that double fire or fire multiple times over the life of the application. The connect once code is shown in Listing 11.

Listing 11. Connect once code
dojo._extconnect = dojo.connect;

dojo.connect = function(source, event, object, method, once){
	source = typeof(source)=="string" ? dojo.byId(source) : source;
	if(!source) throw new Error("Bad source passed to dojo.connect:", source);
	if(once){
		var callback = dojo.hitch(object, method);
		var handle = dojo._extconnect(source, event, function(){
			callback.apply(object, arguments);
			dojo.disconnect(handle);
		});
		return handle;
	}else{
		return dojo._extconnect.apply(dojo, arguments);
	}
}

myObject = {
	onMouseAction: function(evt){
		console.log("mouse action:", evt.type);
	}
};

dojo.ready(function(){
	dojo.connect("btn", "click", myObject, "onMouseAction", true);
});

Code disclaimer

Notice that the examples in this article are used to demonstrate the ability to extend dojo.connect, but are not finalized, production-ready code.

Because true is passed as a fifth argument, the connected method only fires once. Clicking the button a second time has no effect. Notice that the use of once as the fifth argument overwrites the default functionality of don't fix, a functionality that I've never used.

You can wrap these extensions into one master function and add other functionality, such as throttling the firing of events, that can be used to connect to window resize.

Dojo pub/sub

The Dojo pub/sub system follows the computer science pub/sub pattern, which is a simple global collection of callbacks kept in a registry of topics. A simple example of pub/sub code is shown Listing 12.

Listing 12. Simple pub/sub code
dojo.subscribe("data/event/load", function(data){
	console.log("data loaded: ", data)
});
dojo.publish("data/event/load", [{answer:42}]);

Because the collections are global in the Dojo namespace, you can write code that is very loosely coupled. On the other hand, loosely coupled code can be a double-edged sword; if the use of pub/sub is abused, your application can unravel and become difficult to maintain. Remember that loose coupling does not mean lack of structure. In addition, be aware that topics might begin to fire out of order if you have a lot of pub/subs that depend on each other. And if a topic fires or "pre-fires" before you subscribe, you've missed it.

Extending Dojo pub/sub

You can fix pre-firing topics by extending Dojo's topic system to handle a backlog for missed topics; this means that a publish can be fired first, and a subsequent subscribe will still receive the topic when it's registered, as shown in Listing 13.

Listing 13. Code for extending Dojo pub/sub
(function(){
	var topics = {};
	var topicsHandles = {};
	var backlog = {};
	var uid = 0;
	dojo.subscribe = function(topic, object, method){
		var callback = dojo.hitch(object, method);
		if(!topics[topic]) topics[topic] = {};
		var handle = "topic_"+uid++;
		topics[topic][handle] = callback;
		topicsHandles[handle] = topic;
		if(backlog[topic] !== undefined) callback(backlog[topic]);
		return handle;
	}
	dojo.unsubscribe = function(handle){
		delete topics[topicsHandles[handle]][handle];
	}
	dojo.publish = function(topic, data){
		backlog[topic] = data;
		if(topics[topic]){
			for(var nm in topics[topic]){
				topics[topic][nm](data);
			}
		}
	};
})();

// firing our publish with nothing subscribed yet:
dojo.publish("data/event/load", {answer:42});
// act of subscribing will fire topic:
var handle = dojo.subscribe("data/event/load", function(data){
	console.log("data loaded: ", data.answer); // fires right away!
});
// test that a subsequent topic still fires:
dojo.publish("data/event/load", {answer:24});
// unsubscribe from the topic:
dojo.unsubscribe(handle);
// should not fire:
dojo.publish("data/event/load", {answer:99});

Never miss a topic again! Don't be intimidated by the amount of code; it includes the ability to unsubscribe (which explains the use of all the hash maps to track the topics and handles). This is not an extension, but a complete rewrite over the existing methods because Dojo's pub/sub system is so small.

More loosely coupled application

Now you can revisit the application and make it more loosely coupled. To visualize the potential problems with the current structure, change the order of the widgets and watch how out-of-order loading breaks it, as shown in Listing 14.

Listing 14. Out-of-order widgets
<div dojoType="CitySelect" id="citySelect"></div>
<div dojoType="StateSelect" id="stateSelect"></div>

Listing 14 shows a situation where the Dojo parser creates CitySelect, followed by CitySelect's postCreate firing; this calls dijit.byId("stateSelect"), only to fail because StateSelect does not yet exist. The coupling is obviously still too tight if rearranging the widgets breaks the code.

By replacing dojo.connect with pub/sub, you remove the need for the widgets to be aware of each other. And by implementing the backlog-extended pub/sub, you don't have to worry about the order in which things are created or when events begin to fire. Listing 15 shows the final, loosely coupled application code. The extended pub/sub is used but not shown for clarity. Again, see the Download section for all the code.

Listing 15. More loosely coupled code
dojo.declare("StateSelect", [dijit.form.Select], {

	postCreate: function(){
		this.inherited(arguments);
		this.loadData();
	},
	loadData: function(){
		dojo.xhrGet({
			url:"geography.json",
			handleAs:"json",
			load: dojo.hitch(this, "onDataLoaded")
		});
	},
	onDataLoaded: function(data){
		this.data = data;
		this.addOption(this.data.items);
		dojo.publish("data/event/loaded", data);
	},
	onChange: function(value){
		dojo.publish("state/change", value);
	}
});

dojo.declare("CitySelect", [dijit.form.Select], {
	postCreate: function(){
		this.inherited(arguments);
		dojo.subscribe("state/change", this, "onStateChange");
		dojo.subscribe("data/event/loaded", this, "onDataLoaded");
	},
	onDataLoaded: function(data){
		this.data = data;
		this.dataMap = {};
		for(var i = 0;i < this.data.items.length; i++){
			this.dataMap[this.data.items[i].value] = this.data.items[i];
		}
	},
	onStateChange: function(value){
		this.removeOption(this.getOptions());
		var item = this.dataMap[value];
		var menu = item.children;
		this.addOption(menu);
	}
});

// HTML - "backwards" widgets still work!
<div dojoType="CitySelect" id="citySelect"></div>
<div dojoType="StateSelect" id="stateSelect"></div>

Conclusion

JavaScript is already an event-based language, and this article demonstrated new ways to embrace event-based patterns in web applications to accompany it. Dojo provides powerful tools with dojo.connect and pub/sub, which you can modify to make your applications more efficient. You can use these tools to create code that is not only event-based, but also loosely coupled for maximum maintainability.


Download

DescriptionNameSize
All examples used in articleEventDrivenDesignExamples.zip7KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Web development on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development
ArticleID=619629
ArticleTitle=Conquer event-driven design using dojo.connect
publish-date=01252011