Sunday, July 18, 2010

Dojo How To: Publish / Subscribe

I haven't been using Dojo for a very long time, just over a year now, but its time I blog about all the little great features I have learned.

One of the features I like the most in Dojo is the Publish / Subscribe mechanism. Its flexibility allows you to cleanly implement communication between different components like modules, widgets, portlets, etc.

Lets get down to business. Say I have two controllers, a map controller, and the main app controller. The map controller owns the map object in my application, in this case a Google Map object. The app controller owns the communication with the user, browser, AJAX, etc. When my app loads, I want the map to go right to the user's location. The map doesn't care where I get the coordinates from, it just needs the coordinates.


startup: function(){

//some other startup code

//subscribe to the event we will get back from the app when coordinates are available
dojo.subscribe("nael.controller.app.currentPosition", this, this.eventHandlers.updateMapCenter);

//when Im done starting up, yell to the app controller saying Im ready for coordinates
dojo.publish("nael.controller.app.requests",["getBrowserCoordinates"]);
},


So the map widget will initialize the Google Map I'm using, do some other stuff, and when it is done it will publish to the app controller's "nael.controller.app.requests" channel. The message it sends to the channel is an array of arguments. In this case it is the request ["getBrowserCoordinates"]. Your channel names can be anything, I just use the Dojo module path to that widget and end with a good description of the channel, i.e. "requests"

On to the app controller:

The startup function of the widget just subscribes to the required channels

startup: function(){
dojo.subscribe("nael.controller.app.requests",this,this.eventListener);
},


One event listener for the "nael.controller.app.requests" channel. Notice that the listener just passes it to the appropriate event handler, the one we passed in to the channel.

eventListener: function(event){
this.eventHandlers[event]();
},


Finally, the eventHandlers object which will contain all our actual event handlers. Here we have the "getBrowserCoordinates" handler which was the argument ["getBrowserCoordinates"] the map passed into the channel.

eventHandlers:{
getBrowserCoordinates: function(){
if(navigator.geolocation){
navigator.geolocation.getCurrentPosition(function(position){
var coords = {lng:position.coords.longitude, lat:position.coords.latitude};

dojo.publish("nael.controller.app.currentPosition",[coords]);

}
);
}
},

Once the app controller receives the coordinates from the browser's geo location API, it publishes the coordinates to the "nael.controller.app.currentPosition" channel - which the map widget has subscribed to during its startup step. When the map receives that event, it tells the Google Map to re-center around the new point.

I'm doing it this way, to reduce the number of channels each module needs to listen to. I can easily bring the browser to its knees if I have a unique channel for each event I'm thinking of raising. Remember, kitchens get dirty one dish at a time. It makes sense to have one "requests" channel for the main app controller, (or every widget as a matter of fact) that all other components can just send requests to.

You may ask why I have a specific channel for the current position? I guess I could have done it similarly to the requests channel. However, I figured that all widgets will be asking the app controller for stuff, while not all widgets would need to know about the user's current position. If we have a "responses" channel that all widgets subscribed to, it could lead to a lot of unnecessary chatter amongst the widgets and too many event listeners. The second reason, is that the current position channel, may get pretty noisy if it is a mobile browser. Couple both reasons together, and you have a recipe for disaster

So why should you care about JavaScript Publish / Subscribe?


The same reason you would care about it for other technologies. Its a better and much more powerful interface between JavaScript modules. Without these channels and event listeners, you would have just called the getBrowserCoordinates method from the map, or worse, you would have called the GeoLocation API straight from the map. You don't have to use Dojo for this, other libraries also provide you with a pub sub mechanism, like YUI EventTarget. Other JavaScript libraries have it, or have plugins for it. Its a design pattern that makes sense.

Note: if you are still not using a JavaScript library for your web app development, you should seriously reconsider because you are wasting a lot of time re-inventing wheels and light bulbs.

Another example, say you need to add a new widget, instead of trying to figure out where you are all the right places to call a method in this widget from another, you just add the widget and subscribe to the event that is triggered. Change request done.

One final reason, Publish / Subscribe is an excellent way to build a mockup of application workflow. You can stub in some datsabase data and when the backend is ready, you just replace the stub module with the one that will listen to the right request channel, and publish to the right response channel. And as an added bonus, if you screw up the channels, nothing breaks, the messages just won't get passed and you won't see browser errors when functions aren't defined. It fails gracefully - which is important.

1 comment: