Backbone.JS Tutorial part 3

Here’s the 3rd post on the Backbone.js tutorial series. If you missed the first one check it out here. This time I’ll walk you around Backbone.View, the object responsible for creating and updating the UI of the web-app.

Views

Views are used to render and update the UI side of things on the web-app. It is the glue between Backbone.Model/Backbone.Collection and your HTML.

A Backbone.View has several important concepts:

  • template: this is where your dynamic template (Handlebars.js/Moustache.js/Underscore Template/whatever) is loaded - check out how this works by clicking one of the previous links.
  • initialize: as you’ve already figured out, initialize whatever is required here.
  • render: generates the HTML from the template together with your Models and/or Collections. It can even be a static piece of HTML if you wish.
  • events: jQuery style CSS selectors to capture events (click, keypress, etc)

template

First, let’s define our view with an associated template. I’m going to use Underscore Templates.

var view = Backbone.View.extend({
  template: _.template("<h1>Hello Backbone.View</h1><p>Isn't this awesome, Mr. Anderson? Or is it <%= name %>?")
});

And that’s it. You now have a template defined with a dynamic portion on it, the name for this view. At this moment you can’t do nothing cool, but bear with me.

initialize

Initialize is a pretty straight-forward method, except for one important detail that make all the difference between a normal UI and a Backbone.js UI: binding to model and collection events!

var BookView = Backbone.View.extend({
  template: _.template("<h1>Hello Backbone.View</h1><p>Isn't this awesome, Mr. Anderson? Or is it <%= name %>?"),

    initialize: function(){
      this.listenTo(this.model, 'change', function(){alert("It changed!");}.bind(this));
        this.foo = this.options.foo;
        this.bar = this.options.bar;
    }
});

Don’t feel overwhelmed, this can be easily understood :-) If you’re wondering about .bind(this), check this post.

options

The this.options object is where the data that is passed to the view is stored:

> var book_model = new Book({id: 1});
> book_model.fetch(); // here we will get the contents for this book
> var book_view = new BookView({ model: book_model, foo: "foo", bar: "bar"});

The only exception where you don’t have to explicitly capture the objects from the this.options container is when you pass a model, collection, el, id, className, tagName, attributes and events.

listenTo

Remember the Backbone.Model and Backbone.Collection events? This is what this.listenTo is for. You listenTo events triggered by your Model or Collection.

This is the good way to procede because when you invoke view.remove() to remove the View element, the binds that were made through listenTo will also be removed - otherwise you’d have a memory leak in case you forgot to unbind this.

If we changed some attribute on the model:

> book_model.set('new_attribute', 1);
 // Alert is displayed with "It changed!"
> book_model.set('new_attribute', 2);
 // Alert is displayed with "It changed!"

render

Render is where the rendering of the view itself occurs.

But, before entering the render method itself, there are some View attributes that you should know: ###tagName This specifies the tagName that will wrap the code generated by your template. If not defined it will default to <div>. ###className This specifies the class name that will be applied to the view’s tagName. ###el This is where the View’s HTML object will be saved.

And now, of to render()! ##render

Here’s some vanilla code for rendering (let’s assume jQuery is available):

var BookView = Backbone.View.extend({
  template: _.template("<h1>Hello Backbone.View</h1><p>Isn't this awesome, Mr. Anderson? Or is it <%= name %>?"),

    initialize: function(){ (...) },
    render: function(){
      $(this.el).html(this.template(this.model.toJSON()));
        return this;
    }
});

What this is basically doing is to set the view’s el element HTML with the result from the template applied to the model. What you’d do to apply the resulting HTML generated by this view would be something like:

> $('body').append(book_view.render().el);

events

What would a View be without associated events? :-) Backbone allows you to specify jQuery events to interact with your views. Here’s the layout for defining events:

events: {
  'click .btn-eclipse': 'eclipseTheWorld'
}

This translates into: “when a click event is captured on the elements that have the btn-eclipse class, invoke the view’s method eclipseTheWorld. Pretty neat, huh?

Let’s add a new link that would increment the number of pages of the BookModel on this view.

var BookView = Backbone.View.extend({
  template: _.template("<h1> <%= name %> </h1><p>Current number of pages:<%= number_of_pages %></p><a id='btn_add_page' href='#'>Add page</a>"),
    events: {
      'click #btn_add_page':'incNumberOfPages'
    },
    initialize: function(){
      _.bindAll('render', 'incNumberOfPages');
      this.listenTo(this.model, 'change', this.render);
    },
    render: function(){ (...) },
    incNumberOfPages: function(){
      var number = this.model.get('number_of_pages');
        number++;
        this.model.set('number_of_pages', number);
        return false;
    }
});

A few new concepts here, let’s go through them!

  • On the template, we added an anchor text with the id btn_add_page. We then specify that when this element is clicked, to invoke the incNumberOfPages method.
  • _.bindAll is a preemptive way to do the bind(this) trick that underscore.js offers us. This is required since the incNumberOfPages will be triggered in anasync fashion.
  • incNumberOfPages will increase the number of pages for this model.
  • Note that we return false; from the incNumberOfPages method. This happens so that the click event stops there, so it won’t trigger any default click event.

Now the cool part: When we set the attribute, because we are listening to changes on the model, render will be automatically invoked when you press the button. This means the view’s HTML will reflect the change to the number of pages. Pretty sweet, huh?

Next stop will be Backbone.Router, which will handle the navigation in the web-app.