Mediator pattern in AngularJS

Before I left my previous company, I decided to introduce the mediator pattern in order to encapsulate the communication between our data models and the WebSocket. Although the implementation was a bit simple, I think it has a lot of power and future possibilities, making the code cleaner and unit testing live updates easier.

To read more about JavaScript design patterns there is an amazing book written by Addy Osmani and a follow up, where you can understand better the mediator pattern.

A basic mediator provides methods to subscribe, publish and unsubscribe from a channel, keeping track of the channels and the methods that have to be executed on the subscriptors when new content is published to a channel.

angular.module('webApp')
  .service('Mediator', function Mediator() {
    var channels = {}; // Associative object.

    this.publish = function(channel, data) {
      if (! channels[channel]) {
        return;
      }

      var subscribers = channels[channel].slice();

      angular.forEach(subscribers, function(subscriber) {
        subscriber.callback(data);
      });
    };

    this.subscribe = function(channel, id, cb) {
      if (! channels[channel]) {
        channels[channel] = [];
      }

      return channels[channel].push({
        'callback': cb,
        'id': id
      });
    };

    this.unsubscribe = function (channel, id) {
      if (! channels[channel]) {
        return false;
      }
      for(var i = 0, len = channels[channel].length; i < len; i++) { 
        if (channels[channel][i].id === id) { 
          var removed = channels[channel].splice(i, 1); 
          return (removed.length > 0);
        }
      }
      return false;
    };

  });

Our data models then subscribe to a determined channel so they can get updated.

angular.module('webApp')
  .factory('Match', function (Mediator) {
    function Match(data) {
      angular.extend(this, data);
      Mediator.subscribe('match:update', this.id, angular.bind(this, this.update));
    }

    Match.prototype.update = function(updatedMatch) {
      // Update model with new data.
    };

    Match.prototype.close = function() {
      Mediator.unsubscribe('match:update', this.id);
    };

    return Match;
  });

And the socket publishes to the corresponding channel every time that an update is received.

angular.module('webApp')
  .factory('socket', function (Mediator) {
    var socket = io(); // Socket.IO exposes a global io variable.

    socket.on('match:update', function (data) {
      Mediator.publish('match:update', data);
    });

    return socket;
  });

This way we could have several sources of information to update our models, or we could get rid easily of Socket.IO and substitute it with other library, without modifying the code of our data model objects.

Disclaimer: my goal with this implementation was to avoid injecting the vilified $scope to our objects and decoupling the socket events, what worked well for us making unit testing easier.

My implementation of the pattern is similar to Mediator.js that can be easily confused with the publish-subscribe pattern. Therefore a truly implementation of the mediator pattern in AngularJS could look like this angular-mediator.

Advertisements

9 thoughts on “Mediator pattern in AngularJS

    • Hi Nick!

      My idea of the mediator was of an object that encapsulates the communication between other objects and contains some logic to accomplish it. But I trimmed down most of the logic for the example so I think you are right and this looks more like the publish–subscribe pattern.

      I guess the problem is that both patterns are often interchanged and the difference is subtle. For example I’ve found this Mediator.js which looks really similar to my implementation of the pattern.

      So I definitely need to think about it and update this post.

      Cheers, Pablo.

  1. Hi Pablo,
    these functions are great. However, I found one “bug”. If you unsubscribe from a channel inside a publish-event, the direct following listener will never get called. To solve this, it’s necessary to clone the channel-array before calling the listeners. So this is the working solution:

    this.publish = function(channel, data){
    if (!channels[channel]){
    return;
    }
    var subscribers = channels[channel].slice();
    for(var i = 0, length = subscribers.length; i < length; i++){
    subscribers[i].callback(data);
    }
    };

  2. Hi Pablo,
    You are right. I had only two listeners, both listened to the same channel. But the first listener removed his subscription when he was called in the publish-event. Then in the background the second listener got the array-index of the removed (first) listener (0), because the publish-method works with the live subscriber-array with only a reference to it. That’s why the index 1 did’nt exist in the next angular.forEach-call and the second listener never got called. I hope it’s clear now?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s