Views

Views are components for displaying a model or a collection. They oughta be testable in their own behaviour, and although they can at times use other views (e.g.: a view for rendering a list will likely render sub-views for individual entries), they should still work, as in not throw errors, when rendered on their own.

★ ★ ★

Avoid interacting with the DOM indiscriminately. A view’s render method should be the only method that does it, so don’t do stuff like adding/removing class names through other methods when something in the model/collection changes.

If the data changed, just re-render the view. What you can do, however, is make render more complex to accommodate use cases such as, for example, a model is currently being edited and you don’t want to destroy the form when the user is half-way through it. For example:

class BookListView extends Backbone.View
  template: JST['books/list_entry']

  # ...

  render: ->
    if @model.get('editing')
      unless confirm("Model has been modified. Discard changes?")
        return false
    @$el.html @template @model
    @
var BookListView = Backbone.View.extend({
  template: JST['books/list_entry'],

  // ...

  render: function() {
    if (this.model.get('editing')) {
      if (!confirm("Model has been modified. Discard changes?")) {
        return false;
      }
    }
    this.$el.html(this.template(this.model));
    return this;
  }
});

Rendering should be a destructive operation as far as what’s in the container element goes. Calling render by itself should never append anything to the DOM. Instead, it should just clear what’s in the the component element and render the template in again (it’s what happens when you call @$el.html() passing a rendered template object to it).

A good test for this is whether calling render repeatedly on any view breaks it or something else. It shouldn't.

★ ★ ★

Following the previous tip means it’s mostly up to the template to handle every possible state of the model, and things can get hairy if you happen to have a bunch of those. For example, in the following haml-coffee template:

- if @model.get('paid') and not @model.get('returned')
  %p
    This book is paid for! Thanks!
- else if @model.get('overdue')
  %p
    You need to pay for this book immediately!
- else if (@model.get('paid') and @model.get('returned'))
  %p
    You returned this book to the store. Here’s your money back.
# ...
<% if (this.model.get('paid') && (!this.model.get('returned')) { %>
  <p>
    This book is paid for! Thanks!
  </p>
<% } else if (this.model.get('overdue')) { %>
  <p>
    You need to pay for this book immediately!
  </p>
<% } else if (this.model.get('paid') && this.model.get('returned')) { %>
  <p>
    You returned this book to the store. Here’s your money back.
  </p>
// ...

This example happens to be quite slim, but any 4+ tags with a lot of copy that you put in between each state, or maybe every permutation of states meaning a different copy needs to be shown, and you’re looking at a huge template in charge of too much.

There are a few things you can do to mitigate that, but first, understand that if you’re using logic templates (as opposed to a logic-less template format), then the template really is a program and needs to be maintained.

To make a long story short, you should use a presenter (see below for more on that). For the specific problem of too much markup, you can break the template into smaller units and render them as partials (smaller sub-templates) from within the template itself. The presenter will handle the intricacies of that for you.

★ ★ ★

A presenter is a special type of object which you will pass a model to when creating a new instance, and in turn, you’ll pass it to a view. It’ll act as a wrapper for the model/collection, and have only presentation-related logic, and it’s not part of the Backbone suite.

You could write a generic presenter object like this:

# A generic presenter object for Backbone.Model
# =============================================

class ModelPresenter

  # presenter = new ModelPresenter model: model
  constructor: (options = {}) ->
    @model = options.model

  # Pass a template object and it’ll output it with the presenter’s
  # model attributes.
  partial: (template) ->
    template _.extend @, @model.attributes
// A generic presenter object for Backbone.Model
// =============================================

// presenter = new ModelPresenter({model: model});
var ModelPresenter = function(options) {
  var options = options ? options : {};
  this.model = options.model;
}

// Pass a template object and it’ll output it with the presenter’s
// model attributes.
ModelPresenter.prototype.partial = function(template) {
  return template(_.extend(this, this.model.attributes));
}

Then for any particular case, subclass it and add more specific methods:

# A presenter for the Book model
# ==============================

class BookPresenter extends ModelPresenter
  isPaid: ->
    !!@model.get('paid')

  isReturned: ->
    @model.get('paid') and @model.get('returned')
// A presenter for the Book model
// ==============================

var BookPresenter = _.extend(ModelPresenter.prototype, {
  isPaid: function() {
    return !!this.model.get('paid');
  },

  isReturned: function() {
    return this.model.get('paid') && this.model.get('returned');
  }
});

In the corresponding view, instead of passing the model or the model’s attributes directly to a template, create a new presenter and pass it instead, like so:

class BookListView extends Backbone.View
  template: JST['books/list_entry']

  # ...

  render: ->
    presenter = new BookPresenter model: @model
    @$el.html @template presenter
    @
var BookListView = Backbone.View.extend({
  template: JST['books/list_entry'],

  // ...

  render: function() {
    var presenter = new BookPresenter({model: this.model});
    this.$el.html(this.template(presenter))
    return this;
  }
});

Now the template can be vastly simplified:

- if @isPaid()
  = partial JST['books/paid_notice']
- else if @isReturned()
  = partial JST['books/returned_notice']
<% if (this.isPaid()) { %>
  <%= partial(JST['books/paid_notice']) %>
<% } else if (this.isReturned()) { %>
  <%= partial (JST['books/returned_notice']) %>
<% } %>

Or you could go even further and make the partial method in ModelPresenter prettier, though then dependent on the JST object, which contains the templates, being present (pretty certain).

partial: (name) ->
  JST[name] _.extend(@, @model.attributes)
partial: function(name) {
  return JST[name](_.extend(this, this.model.attributes));
}

You can now call partials like so:

- if @isPaid()
  = partial 'books/paid_notice'
<% if (this.isPaid()) %>
  <%= partial('books/paid_notice') %>
<% } %>

This may sound like a lot of work, but it’s all boilerplate you’ll write once and carry with you across projects, so it’s very worth it.

★ ★ ★

I’ve been accessing templates through this object named JST throughout the examples. haml_coffee_assets packages your templates and can export the object in a few different ways, but the idea isn’t new nor exclusive to the tool, and you can get the same thing going with pretty much any other.

We need to get our templates to the views somehow. One simple way to do that is by putting them in an object, accessible via directory-like attribute names. It’s intuitive to just mirror the templates directory structure in it.

That object will need to be visible to your views, which in the case of browserify means you’ll need to export the JST object and require it in every view. Alternatively if you’re using Sprockets (you are if you’re using Rails), just make sure the JST object lives in the window scope.

Unsurprisingly, there are many ways to skin this cat. Tools like requirejs allow you to load templates as dependencies when a view gets used (instanced, even), and it’ll handle caching for you. For Grunt there’s grunt-contrib-jst.

This is one of those points that can be cut short by saying: it doesn’t really matter how you do it. Pick one that sits well with your stack and move on.

★ ★ ★

The correct way to listen to a collection, model, or other kinds of events is by using the listenTo method. Never, ever use model.on 'change'.

So, this is correct:

class MyView extends Backbone.View
  initialize: ->
    @listenTo @model, 'change', @render
var MyView = Backbone.View.extend({
  initialize: function() {
    this.listenTo(this.model, 'change', this.render);
  }
});

While this is wrong:

class MyView extends Backbone.View
  initialize: ->
    @model.on 'change', @render
var MyView = Backbone.View.extend({
  initialize: function() {
    return this.model.on('change', this.render);
  }
});

In the former you’re declaring that the view needs to listen to events on the model, whereas the latter modifies the model itself and ties it forever to the view instance, even if you call remove, which means the memory for the view won’t be freed when you get rid of it.

In short, more accurate to say that the view needs to listen to changes in the model, and listenTo is the way to go.

★ ★ ★

The way a Backbone view works by default, you need to specify a tagName and/or a className. I’m of the opinion that those details should live entirely in the template, since they are markup-related, and that the template should include the root element along with any CSS classes it may have.

What that means is the render method takes a hit in terms of how complex it ends up being, but it’s worth it for the sake of isolation.

render: ->
  presenter = new BookPresenter model: @model
  $newEl = $(@template presenter)
  if @$el[0].tagName is $newEl[0].tagName
    @$el.html $newEl.html()
  else
    @setElement $newEl
  @
render: function() {
  var presenter = new BookPresenter({model: this.model});
  var $newEl = $(this.template(presenter));
  if (this.$el[0].tagName === $newEl[0].tagName) {
    this.$el.html($newEl.html());
  } else {
    this.setElement($newEl);
  }
  return this;
}

Now instead of a template consisting of the view element’s contents, it will be a full representation of it. You can then drop this:

class BookListView extends Backbone.View
  template: JST['books/list_entry']
  tagName: 'li'
  className: 'book-list-entry'
var BookListView = Backbone.View.extend({
  template: JST['books/list_entry'],
  tagName: 'li',
  className: 'book-list-entry'
});

And go with this:

class BookListView extends Backbone.View
  template: JST['books/list_entry']
var BookListView = Backbone.View.extend({
  template: JST['books/list_entry']
});

With the template now containing the root element and its class name along:

%li.book-list-entry
  %h1= @title
  ...
<li class="book-list-entry">
  <h1>
    <%= this.title %>
  </h1>
  …

Bear in mind you may need to copy more than just the markup from newEl, but also it's class attribute.

★ ★ ★

Always handle data changes optimistically. Backbone.Model and Backbone.Collection both provide callbacks for when a change succeeds (returns a 2xx status code) or fails (returns 4xx or 5xx), but you shouldn’t keep the UI waiting until a response comes back to re-render.

When creating a new model, say, after filling a form and submitting it, the model should be immediately placed in a collection (if it belongs in one), and rendered. That’ll happen instantly as far as the user is concerned, which is great. The app will feel a lot snappier, and that’s one of the strengths of a JavaScript application.

If the server errors out for any reason, you can act on that after adding the model to the collection and rendering, perhaps by re-rendering the model and with the view in “editing mode”, and let the user fix what’s wrong in a form again.

You may want, however, to provide a faster way for a user to determine whether a model is valid or not right when submitting the form so there’s no delayed feedback loop. HTML5 provides validations (scroll down to near the end) at the form level. This is excellent if all data input happens through a form, as the browser won’t let the form be submitted until all the fields are valid.

If not, or if HTML5 validations aren’t supported by a browser you’ll want the app to run on, there are model validations. When creating a new model (and adding to a collection), you should check whether the model is valid first before adding it.

A simple model validation looks like this:

# The book model
# ==============

class Book extends Backbone.Model
  validate: (attributes) ->
    if _.isBlank attributes.title
      "Title can't be empty" 
// The book model
// ==============

var Book = Backbone.Model.extend({
  validate: function(attributes) {
    if (_.isBlank(attributes.title)) {
      return "Title can't be empty";
    }
  }
});

The view looks like this (pretend we have an attributesFrom method which pulls text field values from a jQuery form element):

# Component for creating a new book from a form
# =============================================

class NewBookView extends Backbone.View
  template: JST['books/new']
  events:
    'submit .new-book-form': 'submitForm'

  submitForm: (event) ->
    event.preventDefault()
    attributes = @attributesFrom $ event.target
    model = new Book attributes
    if not model.isValid()
      # The error message will be in model.validationError. Ensure
      # the template takes that into consideration.
      @render()
    else
      model.save()
      App.Books.add model
// Component for creating a new book from a form
// =============================================

var NewBookView = Backbone.View.extend({
  template: JST['books/new'],
  events: {
    'submit .new-book-form': 'submitForm'
  },

  submitForm: function(event) {
    event.preventDefault();
    var attributes = this.attributesFrom($(event.target));
    var model = new Book(attributes);
    if (!model.isValid()) {
      // The error message will be in model.validationError. Ensure
      // the template takes that into consideration.
      this.render();
    } else {
      model.save();
      App.Books.add(model);
    }
  }
});

Handling validations for when editing a model is a lot easier, as it’s assumed that the model is already in a collection, so there’s no need to deal that. Just ensure you’re trapping the right event when re-rendering the view.

Assuming BookListView’s template carries a form along to allow for in-place editing:

class BookListView extends Backbone.View
  template: JST['books/list_entry']
  events:
    'submit .edit-book-form': 'submitForm'

  initialize: ->
    @listenTo @model, 'change invalid', @render

  # ...
var BookListView = Backbone.View.extend({
  template: JST['books/list_entry'],
  events: {
    'submit .edit-book-form': 'submitForm'
  },

  initialize: function() {
    this.listenTo(this.model, 'change invalid', this.render);
  },

  // ...
});

As change won’t fire if the model is invalid, you’re declaring that when the model is made invalid, that the component should be rendered again. The template can then ensure the form is displayed with error messages on.

★ ★ ★

Let the boundaries or markup be the limits of what a component touches, and code it so it works stand-alone.

A view’s responsibility ends where the next data point begins. A view for a list of books will be in charge of things like displaying the total amount of books, and of tracking when books get added/removed to/from the collection. It’ll handle the logic for the group of books, but not for the individual books that get rendered by it. For that, it’ll call BookListView, pass a model to it, render that view and append what was rendered to its container, thus delegating the particulars of displaying a specific model to it.

The best way to gauge whether you’re doing things right is really render a view to the screen on its own and see whether it renders and works without throwing errors. Though ensuring the styles look correct being a different story altogether than the one we’re discussing in this book, that’s what you should aim for.

Even the BookListView mentioned before, which is the component for rendering a book in a list of books, it should render correctly when rendered outside of the list, in the “void”, so long as we pass a Book model to it (or a mock of one), and it can find its template. When writing tests for your components, that’s exactly what you will be doing in fact.

★ ★ ★

Speaking of tests, DOM isolation will allow you to test most of a component without writing browser tests. Those are the gnarly, slow tests that everyone’s afraid of. The tests that ensure the logic behind your views and how they interact with your data is sound will run very fast, and they’ll be a lot more numerous than your browser tests, all things being well planned. Mind you, browser tests are necessary, but you’re better off isolating them from the rest, and perhaps running it off your normal testing cycle with a CI server.

So avoid simple pitfalls like reading from the DOM. Always refer to a model/collection for the facts. You’re reading from the DOM when you do things like these:

togglePaid: ->
  if @$el.is '.paid'
    @model.set 'paid', no
togglePaid: function() {
  if (this.$el.is('.paid')) {
    this.model.set('paid', false);
  }
}

Or:

isEmpty: ->
  if @$el.find('.books-list').length is 0
    # ...
isEmpty: function() {
  if (this.$el.find('.books-list').length === 0) {
    // ...
  }
}

That’s useless, and now those views are tied not only to their model/collection, but to the DOM as well, for simple stuff that could be obtained from the data model:

togglePaid: ->
  @model.set 'paid', !model.get('paid')
togglePaid: function() {
  return this.model.set('paid', !model.get('paid'));
}

And:

isEmpty: ->
  if @collection.isEmpty()
    # ...
isEmpty: function() {
  if (this.collection.isEmpty()) {
    // ...
  }
}

Remember: keep the point of contact between a component and the DOM restricted to what goes on in render and to writing to it, never reading.

C J