Loosely Coupled, Reusable UI Components

Originally listed in JavaScript Weekly, 11/2012

This article was initially titled "Loosely Coupled, Reusable UI Components in backbone.js", but I re-titled it, believing that the MVC architectural pattern detailed here has application in any client-side development, whether that's in Backbone, or in a native smartphone app.  The article assumes a fairly good understanding of the Backbone.js MV* library in order to understand code. There is a lot of good material on the web for learning Backbone, including the Backbone home page at backbonejs.org.

One of the things that differentiates Backbone from other frameworks is that it is extremely lightweight. It specifies the framework for doing complex things, including making complex interactive user interfaces, but it doesn't come with any usable UI widgets. The purpose of this article is to talk about some Backbone and JavaScript techniques that might make it possible to build reusable interface libraries to use with custom Backbone apps.

A typical pattern, tight view and model binding:

In a recent application, I added UI to modify the opacity and color ramp palette in a map raster layer. It essentially provides a user interface so that when the user slides the opacity slider, the data displayed becomes transparent, revealing the underlying base map. The application took advantage of Backbone Views, Models and event bindings to structure the components and interaction.

I am wary of trivial Backbone examples, but a simplified example is an opacity control in some sort of image editing app:

tightlyCoupledModelView
tightlyCoupledModelView

When the user slides the slider in the opacity view, it sets the opacity attribute of the image model. This fires a "change" event on the image model, which causes the image display view to update to the right opacity.

The problem with this structure is that what is basically a generic control, used for controlling the level of any numerical attribute, has been tightly couple to an image model, where it only controls opacity. What we'd really like to do is build a generic UI component called slider control, that we could then use for the example above. If we can do this, then we can create plug and play controls that conform to specifications. IE you might replace a slider control with a list-based control that still sets a numerical value.

So as a first shot at generalizing this interface, perhaps we could -parameterize- the Opacity Controller View. Perhaps its constructor takes not only a reference to its model, but also to an attribute parameter to change on that model. So for example, you might have the following instances:

var imageOpacityControlView = new LevelControlView({ model: imageView, attribute: opacity, }); var imageBrightnessControlView = new LevelControlView({ model: imageView, attribute: brightness, }); var image ContrastControlView = new LevelControlView({ model: imageView, attribute: contrast, })

The implementation of this LevelControlView could look like this:

views.LevelControlView = Backbone.View.extend({ initialize: function initLevelControlview() { this.template = this.options.template; this.model = this.options.model; this.attribute = this.options.attribute; }, events: { "change .slider" : "setLevel" }, render: function { this.$el.html(this.template({})); }, setLevel: function(evt) { // in a more advanced version we could map to a parameterized range // of values. For now just set the parameterized attribute //(whether // thats opacity, contrast or brightness or something else). this.model.set(this.attribute, this.$el.find(".slider").val()); } });

This is a pretty good first shot, but it's slightly inflexible. If the view controls more than one model, or a model and a collection, they all have to be added explicitly. When our app changes, and the views must change to accommodate. This is true because there is too tight of a coupling between the views and their models. It also means that we overload people who are good at UI design and engineering with system integration and application architecture tasks. What you really want is to be able to write the view and view model once, in a generic, reusable way, and then forget about it, regardless if other aspects of your app changes.

The Solution

The solution is in two parts, first: lets create a model for our UI view that is dumb and doesn't know anything about the application logic. For the level control above, the model has exactly 1 attribute, level. We then need to wire this up to our image model, so that it can update its own view.

It's tempting to bind the models together directly, but this is probably not a good idea. The problem with this is that now we need to make either the Level Model, or the Image Model smart enough to respond to a change event in another model. There's really no good place for this binding code with this architecture. It's also not good for maintainability and managing complexity.

modelModelBinding
modelModelBinding

View to view model coupling is a pattern that we use a lot, and is well understood. On the other hand it is not good for our models to start listening to other models for change events. You end up with an application graph that looks like a total complex mess because there are no rules for how objects can communicate.

The second part of the solution is to bring the notion of a Controller back into Backbone application development. Let's introduce an Image Controller. Its instance has references to the level model, and to the image model, and it wires these two models together.

controllers.ImageController = { initialize: function(options) { this.opacityControl = options.opacityControlModel; this.image = options.imageDisplayModel; // this binding is what wires the 2 models together through // the setOpacity method. this.opacityControl.on("change", this.onOpacityChange, this); }, onOpacityChange: function() { this.image.set("opacity", this.opacityControl.get("level");) } /* We can do exactly the same thing for an arbitrary number of UI controls like rotation, brightness, contrast, etc. (Of course it is beyond the scope of this exercise, and probably quite difficult to programmatically change some of these parameters because they are not exposed through CSS the way opacity and rotation are. */ };

So what you end up with is

looselyCoupledWithController
looselyCoupledWithController

It's pretty clear how you could use this to wire up lots of disparate generic UI's to their targets. You could have a levelControlModel, a brightnessControlModel and a contrastControlModel all wired up in the ImageController in similar fashion. (It would be up to your excellent, focused UI developer to figure out how you actually create a parameterized image view where you can set contrast and brightness.. no mean feat!)

Conclusion

This does a couple of things for us. First off, when it comes time to debug this application, we know that View -> Model bindings are where they always used to be, but that if there is a more complex binding, we probably need to look to the controller for the feature. The other advantage is that our user interfaces are now entirely decoupled from our application, and can therefor be extracted out to reusable UI libraries, and developed by people who focus on HCI engineering exclusively.

Going a step further, it would be entirely possible to create libraries of reusable UI components that other people could plug into their own apps. Everyone structures their backbone apps a little bit differently, some using Require.js, some using global namespaces. I think if you had a library that played as nicely as possible with all of these different approaches, you could produce a product that would be useful to a lot of different developers. Also it would be produced in a way that the UI was beautiful and highly decoupled from application logic by necessity.