Blog

17
Feb

An Intro to Backbone.js: Part 3 – Binding a Collection to a View

This is Part 3 of a series of tutorials. Here’s Part 1 and Part 2.

So we’ve covered models, collections, controllers, and the basic case of tying a view to a model’s callbacks.

Now we’re going to cover tying a view to a collection. This gets a little hairy, so I’m going to keep it nice and simple to start.

Note: As with most things, there are large number of ways you could do this. If you feel that you have a better way, feel free to reply with it as a comment.

A Basic Collection View

Let’s say that we have an existing page that looks like this:

<header> OMG Donut Shop </header>

<section>
  <header>Donut Selection</header>
  <ul class=donuts></ul>
</section>

I’m going to define a DonutCollectionView that will, when given a collection of donuts, render an UpdatingDonutView for each donut.

This is a pattern I use fairly often. A View that is composed of other views. In this way you can easily reuse functionality between Views.

var DonutCollectionView = Backbone.View.extend({
  initialize : function() {
    var that = this;
    this._donutViews = [];

    this.collection.each(function(donut) {
      that._donutViews.push(new UpdatingDonutView({
        model : donut,
        tagName : 'li'
      }));
    });
  },

  render : function() {
    var that = this;
    // Clear out this element.
    $(this.el).empty();

    // Render each sub-view and append it to the parent view's element.
    _(this._donutViews).each(function(dv) {
      $(that.el).append(dv.render().el);
    });
  }
});

Initialize

I didn’t go into much detail about this before. Backbone’s View, Model, and Collection all have their own constructors (which you don’t override*) that take care of initializing a number of functionalities, and the “initialize” method is run inside of their constructors.

*You could, but you’d better have a good reason.

Creating and Rendering

So let’s say I create a Backbone.Collection of donuts.

var donuts = new Donuts([
  {"name" : "Boston Cream"},
  {"name" : "Lemon-Filled"},
  {"name" : "Rusty Iron Shavings"}
]);

I can then create a View with those Donuts.

var donutCollectionView = new DonutCollectionView({
  collection : donuts,
  el : $('ul.donuts')[0]
});

donutCollectionView.render();

This will render each of the donuts into the View and append them to the ul that I created earlier. Which will end up looking like this:

<header> OMG Donut Shop <header>

<section>
  <header> Donut Selection <header>
  <ul class="donuts">
	<li> Boston Cream </li>
	<li> Lemon-Filled </li>
	<li> Rusty Iron Shavings </li>
  </ul>
</section>

So by virtue of using our UpdatingDonutView, the individual donuts will register changes and rerender.

But adding or removing an item from the collection won’t be reflected. Let’s alter the above DonutCollectionView and hook it into the Collection’s events.

As a reminder, the collection will trigger “add” events each time a model is added to it, and a “remove” event each time a model is removed.

So what would binding the view above to the collection’s events look like?

var DonutCollectionView = Backbone.View.extend({
  initialize : function() {
    // bind the functions 'add' and 'remove' to the view.
    _(this).bindAll('add', 'remove');

    // create an array of donut views to keep track of children
    this._donutViews = [];

    // add each donut to the view
    this.collection.each(this.add);

    // bind this view to the add and remove events of the collection!
    this.collection.bind('add', this.add);
    this.collection.bind('remove', this.remove);
  },

  add : function(donut) {
    // We create an updating donut view for each donut that is added.
    var dv = new UpdatingDonutView({
      tagName : 'li',
      model : donut
    });

    // And add it to the collection so that it's easy to reuse.
    this._donutViews.push(dv);

    // If the view has been rendered, then
    // we immediately append the rendered donut.
    if (this._rendered) {
      $(this.el).append(dv.render().el);
    }
  },

  remove : function(model) {
    var viewToRemove = _(this._donutViews).select(function(cv) { return cv.model === model; })[0];
    this._donutViews = _(this._donutViews).without(viewToRemove);

    if (this._rendered) $(viewToRemove.el).remove();
  },

  render : function() {
    // We keep track of the rendered state of the view
    this._rendered = true;

    $(this.el).empty();

    // Render each Donut View and append them.
    _(this._donutViews).each(function(dv) {
      this.$('ul.donuts').append(dv.render().el);
    });

    return this;
  }
});

The use of this DonutCollectionView is identical to the one above, but now it’s adding and removing properly.

Building a Generic UpdatingCollectionView

Alright, so that’s a lot of code to get an updating collection view for donuts. Maybe not the awesomest thing ever. But does it really need to be tightly coupled to Donuts or DonutViews? Let’s decouple this jerk and make a generic updating view that we can use like this.

var donutCollectionView = new UpdatingCollectionView({
  collection           : donuts,
  childViewConstructor : UpdatingDonutView,
  childViewTagName     : 'li',
  el                   : $('#donut_list')[0]
});

And here’s the implementation of that…

var UpdatingCollectionView = Backbone.View.extend({
  initialize : function(options) {
    _(this).bindAll('add', 'remove');

    if (!options.childViewConstructor) throw "no child view constructor provided";
    if (!options.childViewTagName) throw "no child view tag name provided";

    this._childViewConstructor = options.childViewConstructor;
    this._childViewTagName = options.childViewTagName;

    this._childViews = [];

    this.collection.each(this.add);

    this.collection.bind('add', this.add);
    this.collection.bind('remove', this.remove);
  },

  add : function(model) {
    var childView = new this._childViewConstructor({
      tagName : this._childViewTagName,
      model : model
    });

    this._childViews.push(childView);

    if (this._rendered) {
      $(this.el).append(childView.render().el);
    }
  },

  remove : function(model) {
    var viewToRemove = _(this._childViews).select(function(cv) { return cv.model === model; })[0];
    this._childViews = _(this._childViews).without(viewToRemove);

    if (this._rendered) $(viewToRemove.el).remove();
  },

  render : function() {
    var that = this;
    this._rendered = true;

    $(this.el).empty();

    _(this._childViews).each(function(childView) {
      $(that.el).append(childView.render().el);
    });

    return this;
  }
});

Using the UpdatingCollectionView

So, now that we’ve made a generic updating collection view, let’s go through a potential use case of it!
Let’s say that we’re building a view for a DonutShop.

I’m starting to get nice and opinionated now and I’m going to bring in jQuery template
Here’s a template for a DonutShop

<script id="tmpl-donut_shop" type="template/jquery">
  <header>{{name}}</header>

  <section>
    <header>Donuts offered:</header>
    <ul class=donuts></ul>
  </section>
</script>

And now I’ll declare a view that uses that template.

var DonutShopView = Backbone.View.extend({
  templateId : 'donut_shop',
  initialize : function() {

    // Don't worry about this syntax too much.
    // Basically all that this template bit does is grab a template from jQuery
    // I'll have taken care of compiling the template somewhere earlier.
    this.template =  $.template(this.templateId);
    this._donutCollectionView = new UpdatingCollectionView({
      collection           : this.model.donuts,
      childViewConstructor : UpdatingDonutView,
      childViewTagName     : 'li'
    });
  },

  render : function() {
    $(this.el).empty();

    // And here I use the template to render this object,
    // then take the rendered template 
    // and append it to this view's element.
    $.tmpl(this.template, this.model.toJSON()).appendTo(this.el);

    this._donutCollectionView.el = this.$('.donuts');
    this._donutCollectionView.render();
  }
});

var donutShopView = new DonutShopView({ model : donutShop });

So now we’ve got a donut shop view, which contains an autoupdating collection view, which contains autoupdating model views.

Everything stays in sync.

And everyone is happy.

Next, I will be going over declaratively binding events to views, which is a pretty great thing.


  • Frytaz

    cool, another great post

    • Tooatui

          // Render each Donut View and append them.
          _(this._donutViews).each(function(dv) {
            this.$(‘ul.donuts’).append(dv.render().el);
          });

      Isn’t it better to store all views’ html first, and then append it to the DOM only once

  • Stephen

    I think there’s a slight typo in the last example code block in the ‘Creating and Rendering’ section:

    line 49 starts:

    this.(_donutViews)

    and I think its supposed to be:

    _(this._donutViews)

    without that change I was getting a Parse error and the change seems to line up with what you have in the “Building a Generic UpdatingCollectionView”

    line 46: _(this._childViews)

    But THANK YOU for this tutorial, I’ve only just started looking at Backbone.js and this has been an incredibly helpful set of tutorials.

    • http://twitter.com/n_time Neal Stewart

      Ugh, god. > . <

      Thanks! Fixed.

  • Anonymous

    Thanks for the interesting tutorial, especially the GenericUpdateCollection View. IMHO the handling of nested collections is still one of the weak parts of backbone.js. If we were able to tell a Backbone.Model explicitely which child collections it has, it could set up the whole update notification stuff for us and GenericUpdateCollection would be obsolete.

    Another source of anger is the necessary manual method rebinding: _(this).bindAll(‘add’, ‘remove’);
    Backbone could be smart enough to automatically re-bind the methods of a model/collection for us.

    One remark though: wouldn’t it be more efficient to write

    $(this.el).empty()

    instead of:

    this.$(‘*’).remove();
    $(this.el).html(”);

    • http://twitter.com/n_time Neal Stewart

      There are so many methods in jQuery. Fixed as per your suggestion. :)

    • Anonymous

      re: Another source of anger is the necessary manual method rebinding

      I actually rather like the laissez-faire attitude that backbone takes on. I think it helps the code stay modular and lean. I like having as much flexibility to implement my models as possible.

  • http://twitter.com/MattMueller MattMueller

    Great tutorial! I’m new to this stuff but I was wondering why you don’t use the refresh method every time you update your collection? Couldn’t you bind the refresh event to the view and sync the view that way?

  • Pingback: Quora

  • http://www.facebook.com/jeffrafter Jeff Rafter

    OMG thank you. This should be required reading for anyone using Backbone.

  • Denis Konchekov

    To make jQuery template to work:
    1. You need to include template plugin: http://github.com/jquery/jquery-tmpl .
    2. Change tag in template script: from {{name}} to ${name}.
    3. Change template definition in DonutShopView: from ‘donut_shop’ to ‘#tmpl-donut_shop’.
    4. Pass ‘el’ parameter to DonutShopView constructor:
    var donutShopView = new DonutShopView({ model: donutShop, el: ‘body’ });
    5. Call donutShopView.render();

    • Moosa_es

      I am getting :
      Uncaught TypeError: Property ‘tmpl’ of object # is not a function
      On :
      $.tmpl(this.template, this.model.toJSON()).appendTo(this.el);

      I have the template plugin.
      Any ideas?

      I

  • http://twitter.com/34m0 34m0

    Truely great Backbone tutorial. Have not found any others that cover nested and collection views like you have done.

  • Astro

    It’s still a lot of manual labour for something as fundamental as a collection view. Isn’t there any library for these tasks? I’m using backbone.js myself, and my code is getting very repetitive of all those Updating*Views.

    • Anonymous

      Well go on make your own library) As for me i also dislike this Backbone.js for its stupidity about collection views. It is too much noise about it, but in fact it does really few things – and it’s possible to make them faster by hand (with jQuery and common sense).

      • Thomas Kohler

        do you know why they made this library?

    • Ophir Radnitz

      Backbone Marionette has a decent CollectionView support 
      https://github.com/derickbailey/backbone.marionette

  • jacks

    great series, thanks you very much.

    Eagerly waiting for the next one

  • Anonymous

    Thanks for the article! Now it’s clear how much Backbone sucks.

    • -2

      the article really sux! 

  • http://lucisferre.net Chris Nicola

    Calling the DOM in a loop is a big no-no.  Faster browsers like Chrome, Firefox and IE9 will handle this fairly well, but it will fall flat on it’s face with larger collections in browsers like IE8 and below.  This is not a good practice IMO.

  • Bugsandcrashes

    Regarding the generic updating collection view: if your subview contains any events, you have to call 

    childView.delegateEvents();

    after appending it to the DOM. Otherwise the events won’t fire

    Im a backbone newbie (never showed backbone before) so 
    maybe there is a better fix.

    Great tutorial, thank you.
    Really helped me to get started.

    • Tejas Tank

      Hello,

      I have created two views. each have events

      but for one view events not working but for other working well..

      what should I need to do.. !!!

  • Pingback: Binding collections to views in Backbone.js

  • rob alarcon

    really cool

  • Joakim

    It’s not convention to use “var that = this;”, but rather “var self=this” to maintain scope while nesting methods & functions.

    • Astro

      That is bad practise because self === window. Rather use that.

  • Bartwood
  • Pingback: Bookmarks for May 30th through June 6th | gregs

  • Tooatui

    // Render each Donut View and append them.    _(this._donutViews).each(function(dv) {      this.$(‘ul.donuts’).append(dv.render().el);    });
    Isn’t it better to store all views’ html first, and then append it to the DOM only once?

  • m.

    A thousand THANK YOUs.

  • Сергей Троцюк

    More simple way to do collection view binding - https://github.com/sergey-trotsyuk/Backbone.CollectionElementBinder

  • green55

    yeah agreed with nixmrak – this article really clearly shows that backbone is a complete mess. but if you can twist your mind enough you might actually think its useful…

  • Martin Wintz

    Re-appending views every time you render something seems clunky and unnecessary. A better way to do it is to append all the views once and let them take care of their own rendering.

    So you would put this in DonutCollectionView (pardon the Coffeescript):

    addAllViews: ->
        @collection.each( (donut) =>
            donutElement = $(”).appendTo(@$el)
            new DonutView({model: donut, el: donutElement})
        )

    then in DonutCollectionView.initialize:

    @collection.bind(‘reset’, @addAllViews)

    You can apply a similar concept to the DonutShopView.

    You might not even need a render function for the collection. The collection view’s job is just to maintain the individual element’s views by initializing, adding and removing them.

    If you find yourself using this pattern often consider creating a CollectionView class and extending it with all your CollectionViews. That’s how you take advantage of Backbone’s flexibility.

  • Ian Mackinnon

    The “View.remove” method is already in use for cleaning up a view prior to removing it from the DOM. Redefining it for a different purpose is dangerous.

    http://backbonejs.org/#View-remove

    I would recommend renaming your “remove” method to something else.

  • Jitendra Dixit

    This code is working properly

  • vogomatix

    Thanks for this series of articles. It’s a clear introduction to Backbone and much appreciated