I got to hear some great presentations and have some great conversations at last week’s inaugural Backbone Conf, and one thing that came up over and over again was how to effectively communicate between views in client-side applications. There are lots of patterns for doing this — and we love talking patterns at Bocoup — so I thought I’d go over a few.
For the sake of this conversation, we’ll talk about my favorite demo app, Srchr. When a user enters a search term into Srchr, three things happen:
- the recent searches list is updated
- the request is sent to the server
- when the server responds, the results area is updated
A naive, non-MV*, jQuery-based way to solve this might look like:
mvc-less-srchr.js
$("#searchForm form").submit(function(e) {
e.preventDefault();
var term = $('#searchForm input').val(),
req = $.getJSON('http://search.twitter.com/search.json?' +
'callback=?&q=' + encodeURIComponent(term));
$('#recentSearches').append('<li>' + term + '</li>');
req.then(function(resp) {
var resultsHTML = $.map(resp.results, function(r) {
return '<li>' +
'<p class="tweet">' + r.text + '</p>' +
'<p class="username">' + r.from_user + '</p>' +
'</li>';
}).join('');
$('#searchResults').html(resultsHTML);
});
});
In an MV* land, we throw away this tightly coupled approach, and split our user interface into three distinct views:
- the search form
- the search results list
- the recent searches list
Once we’ve split up our app into these three views, we need to reconnect them so that they interact with each other appropriately. The challenge is to do so in a way that doesn’t reintroduce the tight coupling we saw above. Below, we’ll go over a few of our options.
Pub/Sub
One option is to have views broadcast messages to the entire application when something interesting happens. For example, the search form could announce that a user had searched for something.
You’ll see this work in a couple of ways. In the Backbone Boilerplate, for example, there is an object named app
that is extended with the Backbone.Events
functionality. Views are given access to this object, and they trigger events on it; other views with access to the object can bind to events on it.
You can also use a pub/sub plugin to achieve the same effect.
So, for example, we might do the following in our search form view’s submit handler:
pubsub.js
onSubmit : function(e) {
e.preventDefault();
var term = $.trim(this.$('input').val());
if (!term) { return; }
this.app.trigger('search', term);
}
Then, anywhere else in our app, we could bind to the app object’s search
event, and react accordingly. Generally, we’d give all of our views access to this app object.
Pub/sub is very much a “fire-and-forget” approach — we have no idea whether anything in our application responded to the announcement of the search. This may seem undesirable, but it actually makes unit testing a view incredibly straightforward: we can simply test that a view announced what it should have announced when it should have announced it. It also makes a view’s functionality easy to mock when we’re testing other pieces of code — rather than needing to set up an actual view, we can just trigger an event on the app object.
There are a few potential downsides of pub/sub, however:
- There’s no built-in namespacing for your published “topics” — if another piece of your application starts using the
search
topic for a different meaning, your application is likely to start misbehaving, so it’s up to you to make sure there’s no accidental overlap. - You’ll need to take care that bindings to events on the app object (or to published topics in the more traditional pub/sub model) are properly torn down to avoid memory leaks.
- The fire-and-forget nature of pub/sub can be more global than you’d like it to be — literally any piece of your application that has access to the app object can react to pub/sub announcements.
- Debugging a system where anything can talk to anything can be painful — when things go wrong, the code that breaks can be far from the code that triggered the event.
Evented Views
The shortcomings of a pub/sub approach lead us to a more refined, less global option: evented views. In this pattern, rather than sending messages through an effectively global bus, we trigger events directly on our views; only code that has a reference to a view has an opportunity “hear” the announcement.
In this pattern, we’d start by setting up our views in our route code (or similar):
evented-views-route.js
'/search' : function() {
var searchForm = new SearchForm();
var recentSearches = new RecentSearchesCollection();
var searchResults = new SearchResultsCollection();
new SearchResults({ collection : searchResults });
new RecentSearches({ collection : recentSearches });
searchForm.on('search', function(term) {
recentSearches.add({ term : term });
searchResults.fetch({ add : true, data : { q : term }});
});
}
Then we’d do the following in our search form view’s submit handler:
evented-views-view.js
onSubmit : function(e) {
e.preventDefault();
var term = $.trim(this.$('input').val());
if (!term) { return; }
this.trigger('search', term);
}
This approach lets us ensure that only code that has a reference to our search form can react to its announcements; it also essentially eliminates the namespacing issues of the pub/sub approach. Finally, our bindings to view events will get torn down when the view itself is destroyed.
How well this approach fits into your application will depend on your app’s overall structure and architecture — for example, if you aren’t setting up views in a consistent way, you’ll definitely find this difficult. You also run the risk of tying yourself in knots trying to make your views available in all the places you need them — if you find yourself doing that, it’s probably time to pause and consider a different approach.
Application Model
A third approach is to use one or more models to transmit messages between views by leveraging the events that are triggered when we get
or set
a property on a model. In this pattern, we’d give our views access to an application model, in addition to giving them the models they’ll need to display the appropriate data.
application-model-route.js
'/search' : function() {
var recentSearches = new RecentSearchesCollection();
var searchResults = new SearchResultsCollection();
var applicationState = new Backbone.Model();
new SearchForm({
app : applicationState
});
new SearchResults({
app : applicationState,
collection : searchResults
});
new RecentSearches({
app : applicationState,
collection : recentSearches
});
}
Our search form’s submit handler might then look like this:
application-model-view1.js
onSubmit : function(e) {
e.preventDefault();
var term = $.trim(this.$('input').val());
if (!term) { return; }
this.options.app.set('searchTerm', term);
}
Our search results view would be listening for the application model to change:
application-model-view2.js
initialize : function() {
this.options.app.on('change:searchTerm', function(coll, term) {
this.collection.fetch({ add : true, data : { q : term }});
}, this);
this.collection.on('add', function() {
this.update();
}, this);
}
You might also implement this approach with multiple models for representing different pieces of application state; for example, a User model for keeping track of user information, and a UI model for keeping track of UI state (such as whether a panel is open or closed). Sometimes these models will have a direct relationship to your server-side models, but more frequently, they’ll be used only for managing client-side state, and won’t have a server-side representation.
If we choose this path, we still have our unbinding problem — realistically, this is easily dealt with by adding some convenience methods to our view to help us with smarter binding (and I hear that this may become easier in a future version of Backbone) — but overall, this might be the most MV*-ish way of dealing with relationships between views.
How is this different from a pub/sub approach? In some ways, pub/sub is just a more generic version of the event system used by models, and our application model(s) are ultimately just another method of getting a message from one place to another. Unlike a simple pub/sub pass-through, however, a model can make decisions about when and whether to announce changes. For example, in Ember, related changes are batched and announced all at once, rather than one at a time, and a similar system can be implemented in other MV* frameworks as well. It’s also possible to instruct a model not to announce a change. These are all beneficial features, but they make models a bit less straghtforward to work with than pub/sub.
Which Approach is the Right Approach?
I find that I’m still partial to the second approach — evented views that localize the announcement of interesting view-related occurrences, without having a strong opinion about what those occurrences mean. The pub/sub approach is a bit too global for me in most cases, and while the application model approach is appropriate in many cases, it’s good not to fall into the habit of using models just because you feel like “MV*” says that’s what you’re supposed to do.
In all likelihood, though, your application will use some combination of these methods: perhaps pub/sub for occurrences of truly global interest, evented views for transferring messages within a page, and an application model for information that is meaningful across multiple “pages” of a single-page application.
There is one thing that I feel strongly that you should almost always avoid: views with direct knowledge of other views, unless those views have a clear parent-child relationship. When you tightly couple views, you effectively require that one cannot exist without the other. This increases your testing burden, and decreases reusability. In a parent-child situation, this might make sense, but in other cases, the downsides are abundant.
Finally, I think it remains to be seen whether a library can effectively generalize this portion of client-side app development. Making generalizations about models, routers, and even views is clearly possible, but good decisions about this portion of app development still involves some context-specific decision making, and I think we’re still a ways away from a consensus on the best approach. How are you solving communication between views?
Thanks to Mike and Greg for their detailed feedback; it made this post much better.
Comments
We moved off of Disqus for data privacy and consent concerns, and are currently searching for a new commenting tool.
An approach that’s worked fairly well for me is a combination of global pub/sub and evented views, but all being backed my the same global mediator.
The global mediator can install an interface to an object, optionally with an event prefix that’s used for events fired from that object. \u00a0For a an event \”bar\” on view \”foo\”, you could bind directly to the view’s event interface using .bind( \”bar, .. ) or on the global mediator using .bind( \”foo:bar\”, … )
Good post as usual.
I agree about not tightly coupling the views, it’s usually a better idea to have a mediator in between but for the case above I would use the second approach since it’s simpler and the search list can’t exist without the search field (you can’t search without the field).
In a diff scenario – let’s say a form with many fields that toggles the behavior of the app – the 3rd approach would probably be better since you can listen to individual changes and there is a real need for a centralized model to keep track of the current values and to propagate the changes. That way you could even mock the data of the form during the tests to check the behavior and could test each part separately.
I created a document a couple years ago explaining the pros and cons of diff kinds of event dispatchers: https://github.com/millerme… – I would favor a signal over the other options ;P
Cheers.
Nice post. It clarified some things for me. Something I have been thinking about, but it depends on AMD modules:
AMD’s module.id gives the name of the current module:
define(function(require, exports, module){ var id = module.id; });
A view likely has a constructor, and it will be using a mixin/extend from some other object that can give the view an event emitter capabilities but also allow assigning a unique ID for each instance of that type of view.
viewInstance.on(‘search’) under the covers could translate the topic name to be module.id + ‘:search’, and send the instance ID in the event. This might give a nice blend of pub/sub with evented views: no explicit wiring of views together, but if another view only cares about events from a particular view instance it can check the instance ID on the event.
With the use of module ID prefixes on event/topic names, it helps avoid global collisions. Not a fit for everything, but may be useful in situations that skew towards pub/sub or evented views.
Hi Rebecaa.
Great Post.
In the last project I used event driven views: A View which manages several sub views, registers to its subviews events, and would operate\u00a0accordingly. then, it triggers its own event and by that, notifies to any one who’s listening – which can be a \”parent\” view.I.e, A TableView registers to each RowView events (\”row-select\”, \”row-change\”). the RowView registers to each CellView events (\”change\” or \”select\”).
Finally, the TableView triggers its own event\u00a0respectively, such as: \”table data changed\”, \”row-selected\” etc.
I find this method of registering events to be\u00a0loosely\u00a0coupled in a way.
On the other hand – I also used the Mediator Pattern few times – however – it can be tightly coupled when not used correctly.
Thanks.
Hey Rebecca,
I’m still not sure if I’m totally sold on \”Evented Views\”. From your post you write:
\”In this pattern, rather than sending messages through an effectively global bus, we trigger events directly on our views; only code that has a reference to a view has an opportunity \u201chear\u201d the announcement.\”
and later
\”There is one thing that I feel strongly that you should almost always avoid: views with direct knowledge of other views, unless those views have a clear parent-child relationship. When you tightly couple views, you effectively require that one cannot exist without the other. This increases your testing burden, and decreases reusability.\”
It seems like no other view is really referencing the events coming out of the Search Form view. Instead it updates models which update other views. I would just stress that point because it’s subtle but *extremely* important IMO. Evented views should not reference other evented views. I would say that the third example, the state machine, is probably the best approach for a large-ish application. States are good and people should use them more often.
You mention that pub/sub is hard to debug but I’ve never found that to be the case. It’s so common place in many of the MVC frameworks in Flash, and now Backbone Aura. If you’re namespacing your events by convention you shouldn’t really have problems and I’d argue that you’ll spend less time debugging a shared event dispatcher than you would writing whole objects and building elaborate reference relationships to just facilitate\u00a0communication. As they say, the easiest line of code to debug is the one you never write.
Came here asking on IRC about how to eliminate a global model I was using to trigger app wide events, I guess there’s no clear way of doing it better but for small apps the Application model should be enough