Epoxy provides data binding, which, among many things, allows us to power our most complicated screens with (nearly) no explicit event binding or DOM manipulation. Here's an example of Salsify's product editing controls in action — almost all of the views powering this screen are written without a single line of DOM manipulation, DOM event handling code, or view re-renders:
We instead declare how the UI should look under certain conditions, and Epoxy makes it happen. Today we'll cover data binding, how to get started using Epoxy for data binding, and how to integrate Epoxy with Marionette.
Backbone Views are extremely simple: they only provide a bare minimum framework — Backbone asks a lot of the developer to fill in the holes (how to render a view, how to update the data in the view, how to handle events, etc). While the simple Models and Collections are usable out of the box, Views have to be adapted to work in your environment.
Marionette, in short, fills many of the holes of Backbone in, especially for a single-page app. Where you have to implement render() for each of your Backbone views, Marionette gives you an Renderer abstraction, and calls that during View.render() for you, taking the output and inserting it into the DOM. It removes a lot of the boilerplate one might need to write to get a Backbone app off the ground — if you want to render a model that represents a single item Marionette has an ItemView that makes that easy:
What if you want to render a collection of those items? Marionette also does most of the work for you with a CollectionView. Want to also add some paging logic to that collection's view? You probably want a CompositeView, which is a CollectionView that also functions as an ItemView (for your paging controls, for instance).
Data binding is the process by which application data (usually coming from the application's REST API and stored in a Model) is bound to the presentation (view) of the application. Marionette makes it easy to use templating with Underscore's template method. This makes write-only data binding (that is, writing to the DOM) very easy. If you want to update these values when the model data changes you have to resort to something like this:
Using this markup:
Now, we're stretching here with the example — the view template is so simple here that re-rendering it is trivial. But this isn't always the case. Consider a UI that bulk updates hundreds of URLs — re-rendering all of them starts to become expensive. Scaling the manual data binding also becomes difficult: if all your view data-binds a few attributes of your model like in the example, what happens when you add a new attribute that should be auto-updated? Are you sure to always update the view logic? Update the list of handlers to modify the DOM? Then consider read data binding (pulling data from the DOM):
That view code would be bound to some markup that is something like the following:
Bottom line: this approach just isn't scalable, along multiple axes.
An intro to Epoxy
Epoxy provides easy, declarative view bindings by embedding data-bind attributes in your markup:
Here we'll be binding linkText to both the text child of this anchor tag as well as the title attribute, and linkUrl to be the href. This probably feels a bit like Knockout if you've ever seen it, and Epoxy was certainly influenced by it. Epoxy also allows you to define bindings in your view:
At Salsify we prefer the former and use it everywhere, but it's nice to have options.
Let's dive into the components of what we just described. The bindings definition is just a string: it defines a comma separated list of binding definitions. The left-hand side of each defines the binding handler, and the right-hand side defines the value. The binding handler is just a one-way (or optionally two-way) mapping function between the DOM and the Model--here we used the text handler and attr handler to bind our model to our anchor tag. The right-hand side contains a reference to the value being bound, and it may flow through a binding filter:
Here we decide whether to show this changed label if the link is new or has been edited.
Epoxy also has added the concept of computed attributes — attributes that may be bound like model attributes, but are computed from other attributes:
Now these values are accessible via Epoxy's Model.get('isComplete'), for instance.
Views also support the definition of computed attributes in case you want to define computed values that are entirely for display purposes.
One last thing that Epoxy provides is the concept of a view model. In implementation it's not nothing more than a Backbone or Epoxy Model. But the difference is its intent — it should only store view-specific state. For instance:
And we'll bind it to this bit of markup:
The state of edit controls depends on whether the input editor is open — we don't want to display the icons when the editor is open. Adding this to the data model is not useful — this state is entirely presentation specific; having a separate concept like the View Model is really useful here.
Integrating with Marionette
Marionette is a great library to give structure to your Backbone Views. At Salsify, all of our views derive from either ItemViews, or CompositeViews. When we decided to start using Epoxy, we knew we couldn't throw Marionette out — we had to adapt it to work with Epoxy.
We've always defined our own base classes for all our views. This allows us to mix in functionality that the vast majority of our views want: easy subview registration, view swapability, etc. We have two base classes that are used practically everywhere: BaseItemView and BaseCompositeView. They are mostly the same, so they share a common implementation. Here's an excerpt, with the code to integrate with Epoxy:
And we then create our base views like so:
Above you can see above, we only need to do a few things to get it to work: we need to mix Epoxy in (since we can't subclass it), we need to listen to the ui:bind event that we trigger when Marionette renders the item, and we need to then apply the Epoxy bindings. We also listen to the before:close event to clean up those bindings.
Data binding is great tool, but its also a great tool that can easily be overused. Not all views need data-binding to work. At Salsify we generally use data-binding when it is convenient for the problem at hand, and not when it isn't — we still use straight Underscore templating for some simple markup generation. Data binding can be overused, however. As with most high-level abstractions, there is a performance penalty that should be kept in the back of one's mind. As with all things in software engineering though, we try to build the simplest approach first, and optimize afterwards. Very few times have we needed the optimize step with Epoxy.
Where to go from here
Epoxy's Getting Started tutorial is excellent, complete with a todo list. Beyond that, the API and functionality are fairly simple extensions to Backbone, so diving in with the excellent documentation is probably best.
If you aren't using Marionette (or something similar such as Chaplin) I implore you to take a look. Derrick Bailey didn't give us much with Marionette, but what we did get allows you to solve almost all problems very elegantly with very few tools, and that says a lot.
And finally, if you do decide to use Epoxy, join the conversation on Github. Greg MacWilliam is incredibly responsive with fixes and extensions to the library. Maybe gift him with a pull request when appropriate.