http://www.wintellect.com/blogs/jlikness/building-a-javascript-event-aggregator-using-typescript
Building a JavaScript Event Aggregator using TypeScript
The event aggregator is a useful mechanism for decoupling notifications. Typically, notifications happen through events. In JavaScript, an event is a notification that happens as the result of an action. You can think of it as a notification that is triggered by something. We call the triggering of the event “raising” the event. Events are a simple notification mechanism that contain a collection of handlers. A handler is really just a function that is called when the event is raised. The event may pass some information to the handler, and your function must react to the event.
Perhaps the most popular event in JavaScript is the “click” event. Events can be wired directly in HTML markup, like this:
<a id="homeLink" href="#home" onclick="alert('Go Home!')">Go Home</a>
A more sophisticated way to provide a handler is to do something like this:
var homeLink = document.getElementById('homeLink');
homeLink.onclick = function(e) {
alert('Go Home!');
}
Here, the handler is assigned to the onclick function that is raised when the link is clicked.
Events are the core of interacting with HTML DOM. Many JavaScript frameworks provide a mechanism for you to extend JavaScript objects to participate in the event model as well. For example, using Backbone’s events you can register to an event that is triggered by a business object, and raise the event from the business object. This allows you to have events that are not tied to specific user actions in the DOM.
The event model makes sense for most cases. One complaint about this model is that it does force coupling. To register for an event, you must have a reference to the object or DOM element that triggers the event. You provide your handler directly to the source of the event. This is appropriate in most cases, because coupling isn’t always a bad thing. A common example of this is entering search criteria for a grid and pressing the search button.
The search criteria is often bound to a view model that contains properties for the search criteria. The search event will fire, and this will trigger a call to a service that passes the criteria and returns the results for the grid. While these actions should be decoupled to an extent (i.e. the view model will remain free of view-specific code to keep it testable and maintainable, and the service call is probably implemented in a separate function), it makes sense for the view model to be aware of the service because there is a direct action/reaction taking place – the action is to request the list of results and the reaction is to receive the list. Decoupling these (for example, simply sending a message that states, “I want data” and then having a completely separate mechanism to listen for the message “I have data”) can make the code confusing to follow, hard to troubleshoot and difficult to maintain.
There are a few scenarios such as cross-module communication and extensibility that make more sense from a highly decoupled perspective. For cross-module communication, for example, you may want to use an event aggregator. One module may focus on the process of searching for items and adding them to a shopping cart, while another module handles tracking the shopping cart. You may eventually add a third module and extend the application by providing a shipping estimation calculator that responds with a new total whenever items are added to the cart. In these cases, you can decouple the modules by sending a generic message that an item is selected, then build the other modules to receive that message and act on it.
To facilitate this, you can use the publisher/subscriber model as implemented by the event aggregator. The concept of an event aggregator is simple: you have a “broker” that manages notifications. All modules know about the broker, but not about each other, and publish messages to the broker. The broker maintains a list of subscriptions (modules “subscribe” to messages) and notifies the subscribers when a message is listed.
This pattern is a perfect example of how TypeScript can be used to build a facility without compromising the flexibility of the JavaScript language. TypeScript is a superset of JavaScript, so all valid JavaScript is also valid TypeScript code. Instead of trying to replace the language or force typing, TypeScript allows you to define contracts and types where they make sense and are expected, but retain full control over the power of the dynamic and function-oriented aspects of JavaScript.
I decided to start by developing an MVVM module I’ll call Gom because it’s the glue that we can use on the client to hold things together. I’m exploring this solely as a learning exercise as there are plenty of fantastic frameworks like KnockoutJS, AngularJS, and the framework built into Kendo UI that handle data-binding and enterprise concerns. In TypeScript, a module is like a namespace and can be simply defined like this:
module Gom {
}
That’s it – now I can start defining classes and methods that are specific to my module. The first thing I want to keep track of is subscriptions. A subscription is simply a callback – it’s a function that should be called when a notification is sent (or an event is raised). In this model, I’ll give the subscription an identifier as well so the listener can unsubscribe if it is no longer interested in the notification:
class Subscription {
constructor (
public id: number,
public callback: (payload?: any) => void) {
}
}
Note the signature for a function can be declared using a lambda-style expression, showing the parameters that are expected and the return type. The implementation of the class in JavaScript is a self-invoking function scoped to the Gom module:
var Subscription = (function () {
function Subscription(id, callback) {
this.id = id;
this.callback = callback;
}
return Subscription;
})();
What’s nice is that I don’t have to worry about things like creating a constructor or wrapping the function – TypeScript does that for me. Next, I’ll create an interface for a message. Think of a message as a “channel.” I can send a type of message (giving it a name), and I’ll keep track of all subscriptions for that message. It is assumed that both publishers and subscribers understand how to deal with the content of messages that have the same name. In TypeScript, interfaces have no actual JavaScript implementation. They simply provide compile-time checks, design-time auto-completion and help keep your code clean and make it easier to build and maintain.
interface IMessage {
subscribe(callback: (payload?: any) => void): number;
unSubscribe(id: number): void;
notify(payload?: any): void;
}
The subscription takes a callback which has the signature of expecting any type of object (optional) and returning nothing (this is the function the listener will implement) and returns a number (the token for the subscription so it can be unsubscribed). The unsubscribe function takes the identifier, and the notify function takes an optional payload.
We can now implement the interface:
class Message implements IMessage {
private _subscriptions: Subscription[];
private _nextId: number;
constructor (public message: string) {
this._subscriptions = [];
this._nextId = 0;
}
public subscribe(callback: (payload?: any) => void) {
var subscription = new Subscription(this._nextId++, callback);
this._subscriptions[subscription.id] = subscription;
return subscription.id;
}
public unSubscribe(id: number) {
this._subscriptions[id] = undefined;
}
public notify(payload?: any) {
var index;
for (index = 0; index < this._subscriptions.length; index++) {
if (this._subscriptions[index]) {
this._subscriptions[index].callback(payload);
}
}
}
}
Notice how the message maintains it’s own list of subscriptions. For efficiency, it keeps the subscriptions in their assigned slots and simply sets them to undefined when they are unsubscribed. A notification simply iterates through the array and sends the payload to the subscriptions. Empty slots are skipped.
The implementation in JavaScript is again as self-invoking function that scopes the variables appropriately. Note the public functions are class-wide and therefore assigned to the underlying prototype for the object:
var Message = (function () {
function Message(message) {
this.message = message;
this._subscriptions = [];
this._nextId = 0;
}
Message.prototype.subscribe = function (callback) {
var subscription = new Subscription(this._nextId++, callback);
this._subscriptions[subscription.id] = subscription;
return subscription.id;
};
Message.prototype.unSubscribe = function (id) {
this._subscriptions[id] = undefined;
};
Message.prototype.notify = function (payload) {
var index;
for(index = 0; index < this._subscriptions.length; index++) {
if(this._subscriptions[index]) {
this._subscriptions[index].callback(payload);
}
}
};
return Message;
})();
All of the classes so far have been scoped internally to the module. This means they are local to the self-invoked function that defines the module, but not available externally. You can’t directly create a Message instance. Instead, I expose an EventManager class. This isexported in TypeScript so it can be referenced externally from the module. The event manager handles a list of messages and exposes the various operations:
export class EventManager {
private _messages: any;
constructor () {
this._messages = {};
}
subscribe(message: string, callback: (payload?: any) => void ) {
var msg: IMessage;
msg = this._messages[message] ||
<IMessage>(this._messages[message] = new Message(message));
return msg.subscribe(callback);
}
unSubscribe(message: string, token: number) {
if (this._messages[message]) {
(<IMessage>(this._messages[message])).unSubscribe(token);
}
}
publish(message: string, payload?: any) {
if (this._messages[message]) {
(<IMessage>(this._messages[message])).notify(payload);
}
}
}
Now you can see some more powerful TypeScript features at work. The subscriptions simply exist as properties on the main _messages object. When a subscription comes in, the existence of the Message instance for that subscription is checked, otherwise it is created. The <IMessage> casts the result so you can use auto-complete (type msg and hit the period and you’ll see the list of available methods). This example passes the subscription through to the message.
To unsubscribe the function first checks that the message exists, and only if it does, it passes the token down to unsubscribe. Finally, the publication checks for the existence of subscriptions (if no one subscribed, there is no one to notify) and then passes the payload through. The generated JavaScript (note the assignment to the Gom module so it is available for external consumption):
var EventManager = (function () {
function EventManager() {
this._messages = {
};
}
EventManager.prototype.subscribe = function (message, callback) {
var msg;
msg = this._messages[message] || (this._messages[message] = new Message(message));
return msg.subscribe(callback);
};
EventManager.prototype.unSubscribe = function (message, token) {
if(this._messages[message]) {
((this._messages[message])).unSubscribe(token);
}
};
EventManager.prototype.publish = function (message, payload) {
if(this._messages[message]) {
((this._messages[message])).notify(payload);
}
};
return EventManager;
})();
Gom.EventManager = EventManager;
Now I can demonstrate the pub/sub model through a simple page. Here is the full mark-up. The TypeScript is compiled to a corresponding JavaScript file that is referenced.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>GOM - MVVM Glue for JavaScript</title>
</head>
<body>
<div><input id="txtMessage" value=""/></div>
<div><input type="button" id="btnSubmit" value="Publish"/></div>
<div>
<h1 id="header">Pub/Sub Example</h1>
<p id="paragraph">Type a message in the box and click "publish" to continue.</p>
</div>
<script src="Scripts/GomEvents.js"></script>
<script type="text/javascript">
var events = new Gom.EventManager();
var token = events.subscribe("message", function(msg) {
document.getElementById("header").innerText = msg;
});
(function(tok) {
events.subscribe("message", function(msg) {
document.getElementById("paragraph").innerText = msg;
events.unSubscribe("message", tok);
});
})(token);
document.getElementById("btnSubmit").onclick = function() {
events.publish("message", document.getElementById("txtMessage").value);
};
</script>
</body>
</html>
This is a very simple example but it illustrates the working event aggregator. Two subscriptions are made, one that will listen for the message called “message” and update an H1 tag, another that listens to the same message and updates the P tag. The second listener will also try to unsubscribe the first listener. The result is that the first message will update the H1 and P tags, while subsequent messages only update the P tag. Finally, a click event is wired to obtain the contents of the text box and publish the message. This is of course a contrived example to illustrate the implementation.
While you would never use the event aggregator for such as simple case, let’s go back to the original scenario of a shopping cart. The payload can be any type of object. In the shopping cart example, you could serialize an item added to the cart into JSON, then publish a “cartAdded” message with that as the payload. The cart module could then receive and track that item, while the shipping estimation module receives the same payload and uses it to estimate the shipping costs.
I believe TypeScript made it easier to design and build the implementation by using a class structure, while still generating clean JavaScript code. The resulting code can be consumed by any other code and doesn’t require TypeScript. More importantly, however, developers in a large enterprise project will now be able to reference the module and consume the contents with full IntelliSense, making it a lot easier to explore APIs and understand what they expect. Here’s an example of auto-completion in the IDE – note that I get a list of available functions as well as their complete signature:
You can download the source for this post here. I leave you with this working example:
Click here to view in a new window.
Enjoy!