We talked last time about models and collections. It’s time to move on to Views. Views have quite a bit of responsibility and hold the lion’s share of complexity in an application. In a Rails sense of MVC, they have more in common with controllers as they take care of handing data to the actual views, which are actually templates.
Templates in the pipeline
We decided to go with EJS templates, compiled via the ruby-ejs gem. I learned later about a haml alternative but had a few corner cases where it was flaking out on me. The EJS gem was rock solid and served us well. Can’t go wrong with Sam Stephenson.
Just include the gem in your Gemfile and name your templates with the .ejs
extension, such as your_template.jst.ejs
. This will create a global variable JST
where all of the templates are can be found, precompiled into functions and ready for use inside of views. Don’t forget to add the templates early on in your application.js so the pipeline will pick them up and define JST
before you reference it in your views. We added //= require_tree ./templates
right after our vendor imports in application.js and never thought about it again.
Now that they’re precompiled, we can have a view which looks something like this.
App.Views.MyView = Backbone.View.extend({ template: JST['path/to/template'], initialize: function() { _.bindAll(this); //Ignore this for now =) this.render(); }, render: function() { this.$el.html(this.template({ collection: this.collection })); }, }); //Create the view, probably in your server side template var myView = new App.Views.MyView({ el: "#some-div-to-render-to", collection: new App.Collections.ThingsToRender([{...},{...}]) });
Notice the use of the JST
variable. The key of the JST object matches the path relative to your asset root. We’re calling render as the last step in initialize which means it will render after we create it. You can defer rendering if you wish, but if that’s the case you might been able to defer instantiating the view entirely. I won’t curb your cleverness, but being clever comes at its own expense. Make sure the ROI is there.
Components and composites
The above covers how to build a simple view. However, you may need to decompose views to isolate behaviors and responsibilities as well as reuse them in other places.
There are pages in our application with close to 15 different view classes and several hundred instances of each. An example of this is a two panel layout, where we have a list of items on the left hand side driving the views we see on the right hand side. While this can be done with a single View
it becomes ugly pretty fast.
What we need are two different types of views, each with different goals in mind. In our own terms, we think of these as components and composites. Composite views can be nested within other composites and are responsible for generating any number of components which they manage. Composites may or may not even have an associated template. We didn’t do this strictly via subclassing or anything fancy. We didn’t read a SOLID poster and have it tell us where to draw lines. There’s probably a design pattern I could have referenced, but this abstraction came about naturally from my patented “Do it until it hurts” theory (side note: I have a low tolerance for pain).
Let’s take an example:
App.Views.List = Backbone.View.extend({ initialize: function() { _.bindAll(this); this.render(); }, render: function() { var t = this; this.$el.empty(); this.collection.each(function(item){ var component = new App.Views.ListItem({ model: item }); t.$el.append(component.render()); }); }, }); App.Views.ListItem = Backbone.View.extend({ template: JST['path/to/template'], events: { 'click .delete': 'deleteIt', 'click .spam': 'flagForSpam', 'click .praise': 'praiseItem' }, initialize: function() { _.bindAll(this); }, render: function() { //important to return the $el, so the parent may append it. return this.$el.html(t.template({model: this.model}); }, deleteIt: function(){...}, flagForSpam: function(){...}, praiseItem: function(){...} }); //Create the view, probably in your server side template var myView = new App.Views.List({ el: "#super-sweet-list", collection: new App.Collections.ThingsToRender([{...},{...}]) });
What we have here is a simple List -> ListItem. In the render method of the App.Views.List
we are creating a component view for each element in its collection. We then call render on each component which is responsible for returning its jQuery wrapped $el and append it to our containing element in the List view. We used to have the view spit out an id’ed element for each model and then pass that element to the subview, but this is definitely nicer. All props go to Dayton for that pattern. Before we render the composite we empty it, destroying all of the views should the collection change via add/remove/sort/etc… Backbone does a pretty great job of binding events to a view’s element, so not only are they bound to each individual model preventing the need for html5 data elements, they get cleaned up when you blow the element away.
This may seem inefficient to wipe and recreate the entire list but we were creating and destroying 100s of views and 1000s of elements without issue. There was one instance where efficiency became an issue and we used a more specialized update, but you can use the above pattern until it hurts. Most of the time, you’ll be fine. Also, the ListItem could be contained in another composite view, which is in another composite view, and so on etc…
Sorry for the lag in getting out part 3. In our next and perhaps final journey I’ll talk more about events and communication between views. Having views that know about each other is a tangled mess, but luckily there’s a way to avoid it using a global dispatcher. Stay tuned.
DevMynd is custom software development company with practice areas in digital strategy, human-centered design, UI/UX, and web application and custom mobile development.