Implementing A Publish And Subscribe (Pub/Sub) Service In Angular 2 Beta 14
With Angular 2, we now have a number of event-related classes at our disposal. The EventEmitter handles all of our component output needs and the RxJS library handles, well, tons of other event-stream related functionality. As such, I thought it would be easy to leverage one of these classes in order to implement a Publish and Subscribe (pub/sub) services in Angular 2 Beta 14. But, after noodling on the problem for a while, I couldn't come up with anything that made particle sense. Both of these classes deal with binding and unbinding of event handlers; but, neither does it in a way that aligns well with an event mechanism intended to be consumed by Angular components. I don't want to abandon that idea, though. So, I thought I would to try and implement a simple pub/sub service for Angular 2; then, perhaps as a follow-up post, see how it could be refactored to use one of the existing event-oriented services.
Run this demo in my JavaScript Demos project on GitHub.
I don't want it to sound like I am going against the EventEmitter or RxJS services. Just the opposite - they are both great services and provide loads of excellent functionality for an Angular 2 application. I'm only saying that for a very specific type of pub/sub use-case, neither of them seems to fit the bill.
When I think about pub/sub in an Angular application, I think about it in the context of the component life-cycle. Meaning, when a component is instantiated, I need to bind a number of event handlers; then, when the component is destroyed, I need to unbind all of those same event handlers in order to avoid memory leaks and unpredictable behavior. Both of these actions should be easy; and, I'd like a publish and subscribe service that implements methods that align with this requirement.
In AngularJS 1.x, I would sometimes use the $scope tree as a make-shift publish and subscribe service. While not the most efficient approach, it certainly made event binding incredibly easy. In fact, when using the $scope tree, you never even needed to unbind your event handlers as they were automatically unbound when the $scope [associated with your component] was destroyed.
In Angular 2 Beta 14, we no longer have a $scope tree. But, I'd like to create a service that still affords that kind of ease-of-use. My two main goals would be bulk or fluent event binding and bulk unbinding. This way, no matter how many event handlers I bind within an Angular 2 component, I want to be able to unbind all of them without the chance of forgetting any.
To accomplish this, I came up with a service that has your traditional on/off methods. But, that also allows you to partially apply those methods to a specific context:
// Bind the API to the current component context. This will implicitly associate
// each event handler with the current context. Not only does this apply to the
// invocation of the event handlers - it also allows for mass unbinding.
var boundAPI = pubsub.bind( this )
.on( "eventA", handlerA )
.on( "eventB", handlerB )
.on( "eventC", handlerC )
;
boundAPI.on( "eventD", handlerD );
boundAPI.on( "eventE", handlerE );
Notice that I am calling .bind( context ) before I call any of the .on() methods. The .bind() method returns a version of the API that is partially applied for the given context. Meaning, it will implicitly use the given context object when invoking the various .on() and .off() methods.
On the other end of the component life-cycle, this .bind() approach allows for a simple unbinding of all event handlers bound to the current context:
// Using the API returned by the .bind() method. Unbind all event handlers that
// were bound though the boundAPI.
boundAPI.unbind();
// ... or, using the core pubsub API with an explicit context.
pubsub.unbind( this );
As you can see, unbinding event handlers becomes super simple since it no longer has to mirror the set of bound event handlers. Meaning, you never have to change your .unbind() statement even as you continue to evolve the set of bound event handlers within your Angular 2 component. This is the kind of ease-of-use that I'm looking for.
Let's take a look at this PubSub service in the context of a simple Angular 2 Beta 14 application. In the following code, I can add and remove child components from the active application. Each of these child components uses the PubSub service to both trigger and subscribe to events. As child components are added and removed, we can see that the event bindings are easily managed.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Implementing Publish And Subscribe (Pub/Sub) In Angular 2 Beta 14
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></lin>
</head>
<body>
<h1>
Implementing Publish And Subscribe (Pub/Sub) In Angular 2 Beta 14
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/14/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/14/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/14/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/14/angular2-all.umd.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/14/almond.js"></script>
<script type="text/javascript">
// Defer bootstrapping until all of the components have been declared.
requirejs(
[ /* Using require() for better readability. */ ],
function run() {
ng.platform.browser.bootstrap(
require( "App" ),
[
require( "PubSub" )
]
);
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the root application component.
define(
"App",
function registerApp() {
// Configure the App component definition.
ng.core
.Component({
selector: "my-app",
directives: [ require( "PubChild" ) ],
template:
`
<p>
<a (click)="addChild()">Add child</a> —
<a (click)="removeChild()">Remove child</a>
</p>
<pub-child *ngFor="#i of childCount" [index]="i"></pub-child>
`
})
.Class({
constructor: AppController
})
;
return( AppController );
// I control the App component.
function AppController() {
var vm = this;
// I hold the INDICES for the child components. This is just a HACK
// to get [ngFor] to execute an index-loop as opposed to an array
// loop (array -> [ 0, 1, 2, 3, ... ]).
vm.childCount = [];
// Expose the public methods.
vm.addChild = addChild;
vm.removeChild = removeChild;
// ---
// PUBLIC METHODS.
// ---
// I add a child component index.
function addChild() {
vm.childCount.push( vm.childCount.length );
}
// I remove the last child component index.
function removeChild() {
vm.childCount.pop();
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the PubChild component. This component both emits PubSub events
// and listens for PubSub events.
define(
"PubChild",
function registerPubChild() {
// Configure the PubChild component definition.
ng.core
.Component({
selector: "pub-child",
inputs: [ "index" ],
template:
`
<strong>Child {{ index }}</strong>
— <a (click)="triggerEvent( 'foo' )">Trigger Foo</a>
— <a (click)="triggerEvent( 'bar' )">Trigger Bar</a>
`
})
.Class({
constructor: PubChildController,
// Define the life-cycle methods on the prototype so that they
// are picked up at runtime.
ngOnDestroy: function noop() {}
})
;
PubChildController.parameters = [
new ng.core.Inject( require( "PubSub" ) )
];
return( PubChildController );
// I control the PubChild component.
function PubChildController( pubsub ) {
var vm = this;
// When setting up the PubSub event handlers, we're going to use the
// .bind() method to create an instance of the API that auto-binds
// all of the subsequent event handlers to the given context (this
// component). This allows us to simplify our .on() method calls as
// well as setting us up to call .unbind() during the DESTROY event.
pubsub.bind( vm )
.on( "foo", handlePubSubEvent /* , implicit vm via .bind() */ )
.on( "bar", handlePubSubEvent /* , implicit vm via .bind() */ )
;
// Expose the public methods.
vm.ngOnDestroy = ngOnDestroy;
vm.triggerEvent = triggerEvent;
// ---
// PUBLIC METHODS.
// ---
// I get called when the component is being destroyed.
function ngOnDestroy() {
// When the component is destroyed, we need to unbind its PubSub
// event handlers so they don't create a memory leak or lead to
// unexpected behavior. And, since we used the .bind() call to
// set up the bindings, we ensured that all of our event handlers
// were bound to the current component context. As such, we can
// quickly and easily unbind ALL event handlers related to this
// component using .unbind().
pubsub.unbind( vm );
// NOTE: If we had not bound the handlers to a specific context,
// we could have still destroyed them individually.
// --
// pubsub.off( "foo", handlePubSubEvent );
// pubsub.off( "bar", handlePubSubEvent );
}
// I trigger the given PubSub event.
function triggerEvent( eventType ) {
console.warn( "PubChild-%s about to trigger [%s].", vm.index, eventType );
pubsub.trigger( eventType, ( "PubChild-" + vm.index ) );
}
// ---
// PRIVATE METHODS.
// ---
// I handle PubSub events.
// --
// NOTE: For this demo, we're only logging the event type, so we
// don't actually need an event-specific handler.
function handlePubSubEvent( event, originChild, anotherArg ) {
console.log( "PubChild-%s handling [%s] from %s.", vm.index, event.type, originChild );
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a simple publish and subscribe (pub/sub) service.
define(
"PubSub",
function registerPubSub() {
return( PubSub );
// I am a simple Publish and Subscribe (pub/sub) service that is geared
// for use in the Angular component life-cycle in which event handlers
// need to be associated with a component at init time; then, all of
// those same event handlers need to be unbound at destroy time.
function PubSub() {
// I hold all of the event bindings.
// --
// NOTE: I am using a simple array rather than a hash of objects
// based on event types (which is more commonly done) in order to
// keep the code as simple as possible.
var bindings = [];
// Return the public API.
return({
bind: bind,
off: off,
on: on,
trigger: trigger,
unbind: unbind
});
// ---
// PUBLIC METHODS.
// ---
// I return a version of the API in which all of the [relevant]
// methods are pre-bound to the given context. This allows you to
// bind all event handlers to the given context; but, perhaps more
// conveniently, unbind all event handlers for a given context with
// one .unbind( context ) method invocation.
function bind( context ) {
return({
// Simple pass-through methods.
bind: bind,
trigger: trigger,
// Bound version of the API methods.
// --
// CAUTION: All of these methods return [this] which points
// to the pre-bound version of the API, not the original API.
off: function boundOff( eventType, eventHandler ) {
off( eventType, eventHandler, context );
return( this );
},
on: function boundOn( eventType, eventHandler ) {
on( eventType, eventHandler, context );
return( this );
},
unbind: function boundUnbind() {
unbind( context );
return( this );
}
});
}
// I remove the given event binding configuration.
function off( eventType, eventHandler, context ) {
bindings = bindings.filter(
function operator( binding ) {
return(
( binding.eventType !== eventType ) ||
( binding.eventHandler !== eventHandler ) ||
( binding.context !== ( context || null ) )
);
}
);
// CAUTION: [this] is the publicly exposed API object.
return( this );
}
// I add the given event binding configuration.
function on( eventType, eventHandler, context ) {
bindings.push({
eventType: eventType,
eventHandler: eventHandler,
context: ( context || null )
});
// CAUTION: [this] is the publicly exposed API object.
return( this );
}
// I trigger the given event with the given arguments. An event can
// be triggered with any number of arguments that are all passed to
// the event handlers in the same order.
function trigger( eventType /*, eventArguments 1..N */ ) {
var eventArguments = Array.prototype.slice.call( arguments );
// Replace the first argument, which is currently a simple
// eventType string, with an event object which composes the
// eventType. This way, when we trigger the event handlers, the
// first trigger argument will be the event object. While we are
// not using it now, this gives us an opportunity to add event
// methods like .stopPropagation().
var event = eventArguments[ 0 ] = {
type: eventType,
createdAt: new Date()
};
bindings.forEach(
function iterator( binding ) {
if ( binding.eventType == eventType ) {
binding.eventHandler.apply( binding.context, eventArguments );
}
}
);
return( event );
}
// I unbind all of the event handlers for the given context.
function unbind( context ) {
bindings = bindings.filter(
function operator( binding ) {
return( binding.context !== context );
}
);
// CAUTION: [this] is the publicly exposed API object.
return( this );
}
}
}
);
</script>
</body>
</html>
As you can see, each PubChild component uses the .bind() method when setting up its event handlers. Then, in the ngOnDestroy() component life-cycle event, it simply calls .unbind(). In doing so, all if its bound event handlers are correctly cleaned up. Because of this, we can see that, as PubChild components are added and removed, the list of console log statements accurately reflects the list of active components:
I'm sure that there are a ton of existing Pub/Sub style libraries out there already. So, it may seem silly or even foolish to try and create one yourself. But, I really wanted a publish and subscribe API that aligned well with the Angular 2 component life-cycle. And, since publish and subscribe code doesn't have to be overly complicated, it can be worth it to write something from scratch that perfectly suits your needs.
Want to use code from this post? Check out the license.
Reader Comments
Can you elaborate a bit why EventEmitter does not fit your application?
@Braim,
Good question. For starters, I believe it is still up in the air as to whether or not the EventEmitter will be an "observable object" in the final release of Angular 2:
www.bennadel.com/blog/3045-ward-bell-do-not-expect-eventemitter-to-be-observable-in-angular-2.htm
That said, let's keep this conversation to the RxJS Observable interface since that is a "know quantity." If all I wanted to do was map an event-type to a set of callbacks, then I think something like the Subject would have worked out quite well (pseudo code):
bindings[ event_type ] = new Subject();
bindings[ event_type ].subscribe( someCallback );
bindings[ event_type ].next( someValue );
bindings[ event_type ].unsubscribe( someCallback );
And, in fact, that would have been a really simple implementation. But, ultimately, I wanted to implement a little more life-cycle-oriented functionality on top of that general workflow. Specifically, I wanted to be able to unbind a group of callbacks there were all bound by a single Component. However, in order to do that, I need to "own" the list of subscriptions made by a given Component.
To do that, I would have still had to store a list of subscriptions; and, at that point, it just didn't like the Subject would have been adding any value on top of just invoking methods directly.
Hey Ben,
I was recently thinking of a simpler way to achieve this simpler cleanup (apologies for the repetition there). A possible way would be a Class to be extended, that implements the hooks we need, and circumventing method overwriting.
Something like this:
export class SafeUnsubscriber implements OnDestroy {
private subscriptions: Subscription[] = [];
constructor() {
let f = this.ngOnDestroy;
this.ngOnDestroy = () => {
f();
this.unsubscribeAll();
};
}
protected safeSubscription (sub: Subscription): Subscription {
this.subscriptions.push(sub);
return sub;
}
private unsubscribeAll() {
this.subscriptions.forEach(element => {
!element.isUnsubscribed && element.unsubscribe();
});
}
ngOnDestroy() {
// no-op
}
}
I'd say allowing for overwriting ngOnDestroy, or requiring super.ngOnDestroy(), doesn't make sense here.
Would really appreciate your take on this :)
@Tiago,
If I understand what you are saying, I think you are asking about a Component extending your subscription objects like:
class MyComponent extends SafeUnsubscriber { ... }
It's an interesting idea. But, I think this might be a misuse of object inheritance. The component is not really a "type of" unsubscriber. The subscription functionality is really just a behavior that the component wants to implement. I think you'd run into issue when you want to the component to _extend_ other classes as well.
I'll say that I'm not really a good OOP programmer, but here's a quote from Elegant Objects, a book I recently read:
= = = = = =
Inheritance, intuitively, is a top-down process, where child classes inherit code from parent classes. Method overriding makes it possible for a parent class to access the code of child class. Such reverse thinking goes against common sense, so to speak.... However, there is a solution. Just make your classes and methods either final or abstract, and the very possibility of a problem fades away.
If you follow this principle and make all your classes final or abstract, you will almost never use inheritance. But sometimes you will, when it makes sense. When does make sense? Only when you need to refine class behavior, not extend, but refine. There is a difference. Extending means that an existing behavior is partially supplemented by a new one. Refining means that partially incomplete behavior is made complete.
= = = = = =
Regarding this quote, I think you are attempting to have the component "extend" the subscription where as people "smart than me" seem to think inheritance should be used to "refine" a behavior.
But mostly, I'm not confident in my answer :D
@Ben,
You're definitely correct, this is an intended subversion of the Object Oriented paradigm as we know from OOP languages. But then again, Classes in Typescript don't implement it correctly (and neither does ES6, as it is still a prototypal inheritance model).
Regarding extension of other classes, that was why I felt the need to replace the ngOnDestroy method in the constructor (bypassing any inheritance problems).
Thanks for the input! It is tricky to try and follow "proper" inheritance in JS/TS, Decorators might be the right way here, as well!
@Tiago,
I'm still learning as I go, especially with some of the object stuff. But, I'm trying to embrace composition over inheritance when I can, which is why I am enjoying passing the PubSub system into the various components. But, I haven't written a *single-line* of projection Angular 2 code yet ;) So, it's still a work in progress :D