Simplifying View-View Events in Backbone using the Mediator Pattern

June 15, 2012 Mark Rushakoff

In most single-page apps, you will inevitably end up having multiple views on one page at a time.
It usually starts out with just one view, and then that view needs to bind to events on a subview, and then the subview gets its own subview who will also trigger events that affect the top-level view…

When you have one view that ties to one model, the standard way to set up the binding in Backbone looks something like

MyView = Backbone.View.extend({
  initialize: function() {
    this.model.bind("change:someProp", this.render, this);
  }
  // ...
});

You might later find yourself binding view events in the same way:

MySubView = Backbone.View.extend({
  events: {
    "click li.foo": "fooSelected"
  },

  fooSelected: function(e) {
    this.trigger("foo:selected", new Foo({id: $(e.target).data("fooId") }));
  }
  // ...
});

MyView = Backbone.View.extend({
  initialize: function() {
    this.model.bind("change:someProp", this.render, this);
    this.subview = new MySubView();
    this.subview.bind("foo:selected", this.addFoo, this);
  },

  addFoo: function(foo) {
    var fooView = new FooView({model: foo});
    fooView.bind("someOtherEvent", this.render, this);
    this.$(".foos").append(fooView.render().$el);
  }
  // ...
});

This can get out of hand rather quickly in several ways:

  • You have to add a lot of code to your view to just to wire up the events properly, and your view has to be very aware of specific subviews
  • If you have three or more levels of views, then the views in the middle may need to “forward” events between subviews and parent views
  • You can end up binding the same method to the same event on subviews of the same class
  • To test that the events are configured correctly, you will need to instantiate all of the necessary subviews in your tests

In my experience, one of the cleanest solutions to this problem is to apply the mediator pattern.

(To be fair: it would be better if we could avoid needing a mediator altogether, but that is often not a realistic goal.)

The Mediator Pattern

Concept

The mediator is a centralized object (often a singleton) whose public API looks roughly like:

  • subscribe(string channel, function callback)
  • publish(string channel, arguments)
  • unsubscribe(string channel, function callback)

Even though your views are still coupled together through their layout hierarchy, your event logic is coupled completely around the mediator.
The good news is that your view events just became enormously easier to test.

Testing

The naive way to test view events before introducing a mediator may look something like this, in Jasmine:

describe("the subview", function() {
  describe("the foo:selected event", function() {
    var spy;
    beforeEach(function() {
      spy = jasmine.createSpy();
      subview.bind("foo:selected", spy);
    });

    it("is triggered when clicking on a li.foo", function() {
      subview.$("li.foo:eq(0)").click();
      expect(spy).toHaveBeenCalled();
    });
  });
});

describe("the view", function() {
  describe("the foo:selected event", function() {
    beforeEach(function() {
      spyOn(view, "fooSelected");
      view.subview.configure();
      view.render();
    });

    it("calls fooSelected", function() {
      view.subview.trigger("foo:selected");
      expect(view.fooSelected).toHaveBeenCalled();
    });
  });
});

It looks fairly simple when written this way, but in the real world, getting your views all set up and rendered may be significantly more complicated.

After introducing a mediator, you only need to test the interaction between your view and the mediator.
If you write some custom matchers, your tests can look as clean as this:

describe("the subview", function() {
  describe("the foo:selected event", function() {
    it("is triggered when clicking on a li.foo", function() {
      expect(function() {
        subview.$("li.foo:eq(0)").click();
      }).toPublish("foo:selected");
    });
  });
});

describe("the view", function() {
  describe("the foo:selected event", function() {
    it("calls fooSelected", function() {
      spyOn(view, "fooSelected");
      Mediator.publish("foo:selected");
      expect(view.fooSelected).toHaveBeenCalled();
    });
  });
});

Existing libraries

There are a decent number of libraries out there that do mediation and only mediation.
Surveying some of the libraries listed on microjs turns up several:

A quick Google search will turn up even more libraries:

But we’re already using Backbone…

Exactly!
You don’t need any external libraries because Backbone already provides everything you need to implement the mediator pattern.
Backbone.Events is what provides the existing bind/unbind/trigger functionality, which is everything you need to make your own mediator.
In fact, they even briefly mention this concept in the Backbone docs:

For example, to make a handy event dispatcher that can coordinate events among different areas of your application: var dispatcher = _.clone(Backbone.Events).

If you do choose to use the mediator pattern, here are some other extensions to that basic pattern that may be helpful:

  • Consider what you want your mediator to do when the same function is bound twice to the same event name.
    In some cases this is correct and it should call that function twice, but in other situations that is a programming error.
    Consider also when the function is the same but the context differs.
  • Sometimes a bindOnce function is handy – unsubscribe immediately after invoking the callback.
  • Make sure that both in the app (”between” pages) and in your tests (between specs) you unsubscribe all existing subscriptions so that you don’t leak references to objects that you are no longer using.
  • Depending on how you implement your mediator, you’ll probably want to write a custom matcher for your test library as well.

About the Author

Biography

Previous
Android Roundup: Week of May 14, 2012
Android Roundup: Week of May 14, 2012

Cool things this week: Having an application continue to track the user’s GPS location while backgrounded....

Next
iOS Roundup: Week of May 14, 2012
iOS Roundup: Week of May 14, 2012

Cool things this week: Christopher’s Objective-C helper classes: XTL, XTL_Framerate, XTL_Debug, XTL_Device...

×

Subscribe to our Newsletter

!
Thank you!
Error - something went wrong!