14. Consuming events from the model layer

There are two areas of interest in the chapter, both of which fall under the larger issue of generating and consuming events:

Re-rendering views in response to data changes. When data changes, we get a change event from the model layer. In response, we would like to re-render all the views that were affected by the change.

Communication between views. Some actions - like the example in Gmail where you change the display density - require that multiple views change in response to the same user event. We need to pick a nice way to represent these changes, and trigger the right changes.

These are both coordination problems. We want to specify that when A happens, B and C should also happen. In essence, we are trying to bridge the gap between concrete DOM events and event handlers spread accross multiple views:

[Events] < - > [Event handlers / actions]

Let’s assume that we are trying to implement the following interaction: “when a user selects a message, the top menu should change and the message should appear as selected in the message list view”. In this scenario, we are changing a piece of data and asking for that change to be communicated to multiple views.

There are several ways we could do this:

Directly in the select event handler. The naive and obvious way would be to write code in the list that explicitly calls the interested parties.

MessageView.onSelect = function() {
  message.setSelected(true);
  list.check(message.id);
  menu.update(message);
  // one call for each other view that cares about this operation
};

However, the problem is that this is highly likely to lead to breakage since the views are tightly coupled: the messageview knows about the message model, the list view and the menu view.

Using a mediating controller One way is to use a mediating controller, which refers to the objects directly. This looks something like this:

MessageView.onSelect = function() {
  controller.selectMessage();
};

Controller.selectMessage = function(message) {
  message.setSelected(true);
  list.check(message.id);
  menu.update(message);
  // one call for each other view that cares about this operation
};

Now, instead of views knowing about each other, they only need to know about a controller. Putting the code in a controller centralizes the coordination, but the code is still ugly and fragile: since that code explicitly refers to each view, removing or breaking one view can potentially break the whole re-render. It’s still the same code, you just moved it into a different object; this is just as fragile as without a mediating controller (since the controller can’t work without both views), though it is a bit more reusable since you can swap the controller.

Using observables. Another alternative is to use observables. When someone selects a message, we can reflect that either as a property of the message (“selected”) or as part of a collection (“selectedMessages”):

Here is how this might look as code:

MessageView.onSelect = function() {
  AppModule.FooController.set('currentFoo', this);
  // currentFoo is a observable property
  // each otherView observes it, and performs
  // actions based on change events
};

// init is called when the other view is created
OtherView.init = function() {
  Framework
    .observe('AppModule.FooController.currentFoo')
    .on('change', function(model) {
      OtherView.update(model);
    });
};

While the views don’t know about each other, they still know about the controller. Furthermore, the properties of the controller become an implicit API between the views. I say implicit, because the controller doesn’t know it’s being used for this purpose. So instead of having an explicit controller function that knows about the views, you now have a controller property that is the API for making calls between views and for passing state between views. You haven’t gotten rid of “views knowing about the controller”; it’s just that the views are now also responsible for registering callbacks on the controller properties.

Of course, in the case of one view, another dependent view and one controller this isn’t too bad. But the problem is that as the number of views, controllers and interrelationships increase, the number global state properties and dependencies on various pieces of state increases.

Using global events. We can also implement this using a global event dispatcher / event aggregator. In this case, selection is reflected as an global event. When the user selects a message, a global event is emitted. Views subscribe to the selection event to be notified and can publish messages via the global event emitter instead of knowing about each other.

MessageView.onSelect = function() {
  global.emit('message_selected', this);
};

OtherView.init = function() {
  global.on('message_selected', function(model) {
    message.setSelected(true);
  });
};

The global event emitter is the single source of events for the views. Views can register interest in a particular event on the global event emitter, and models can emit events on the global event emitter when they change. Additionally, views can send messages to each other via the global event emitter without having an observable property change.