user2545975
user2545975

Reputation: 33

BackboneJS: View renders fine, but refreshes with undefined collection

I am messing around with Backbone some weeks now and made some simple applications based on tutorials. Now I started from scratch again and tried to use the nice features Backbone offers as I am supposed to.

My view gets in the way though. When the page loads, it renders fine and creates its nested views by iterating the collection. When I call render() again to refresh the whole list of just a single entry, all of the views attributes seem to be undefined.

The model of a single entry:

Entry = Backbone.Model.extend({
});

A list of entries: (json.html is placeholder for dataside)

EntryCollection = Backbone.Collection.extend({
    model: Entry, 
    url: 'json.html'
});

var entries = new EntryCollection();

View for a single entry, which fills the Underscore template and should re-render itself, when the model changes.

EntryView = Backbone.View.extend({
    template: _.template($('#entry-template').html()), 
    initialize: function(){
        this.model.on('change', this.render);
    },
    render: function(){
        this.$el.html(this.template(this.model.toJSON()));
        return this;
    }
});

View for the whole list of entries which renders a EntryView for each item in the collection and should re-render itself, if a new item is added. The button is there for testing purposes.

EntryListView = Backbone.View.extend({
    tagName: 'div',
    collection: entries, 
    events: {
        'click button': 'addEntry'
    },
    initialize: function(){
        this.collection.on('add',this.render);
    },
    render: function(){
        this.$el.append('<button>New</button>');  //to test what happens when a new item is added
        var els = [];
        this.collection.each(function(item){
            els.push(new EntryView({model:item}).render().el);
        });
        this.$el.append(els);
        $('#entries').html(this.el);
        return this;
    },
    addEntry: function(){
        entries.add(new Entry({
            title: "New entry", 
            text: "This entry was inserted after the view was rendered"
        }));
    }
});

Now, if I fetch the collection from the server, the views render fine:

entries.fetch({
    success: function(model,response){
        new EntryListView().render();
    }
});

As soon as I click the button to add an item to the collection, the event handler on EntryListView catches the 'add' event and calls render(). But if I set a breakpoint in the render function, I can see that all attributes seem to be "undefined". There's no el, there's no collection...

Where am I going wrong? Thanks for your assistance,

Robert

Upvotes: 3

Views: 2797

Answers (1)

nikoshr
nikoshr

Reputation: 33344

As is, EntryListView.render is not bound to a specific context, which means that the scope (this) is set by the caller : when you click on your button, this is set to your collection.

You have multiple options to solve your problem:

  1. specify the correct context as third argument when applying on

    initialize: function(){
        this.collection.on('add', this.render, this);
    },
    
  2. bind your render function to your view with _.bindAll:

    initialize: function(){
        _.bindAll(this, 'render');
        this.collection.on('add', this.render);
    },
    
  3. use listenTo to give your function the correct context when called

    initialize: function(){
        this.listenTo(this.collection, 'add', this.render);
    },
    

You usually would do 2 or/and 3, _.bindAll giving you a guaranteed context, listenTo having added benefits when you destroy your views

initialize: function(){
    _.bindAll(this, 'render');
    this.listenTo(this.collection, 'add', this.render);
},

And if I may:

  • don't create your main view in a fetch callback, keep it referenced somewhere so you can manipulate it at a later time
  • don't declare collections/models on the prototype of your views, pass them as arguments
  • don't hardwire your DOM elements in your views, pass them as arguments

Something like

var EntryListView = Backbone.View.extend({
    events: {
        'click button': 'addEntry'
    },
    initialize: function(){
        _.bindAll(this, 'render');
        this.listenTo(this.collection, 'reset', this.render);
        this.listenTo(this.collection, 'add', this.render);
    },
    render: function(){
        var els = [];
        this.collection.each(function(item){
            els.push(new EntryView({model:item}).render().el);
        });

        this.$el.empty();
        this.$el.append(els);
        this.$el.append('<button>New</button>');
        return this;
    },
    addEntry: function(){
        entries.add(new Entry({
            title: "New entry", 
            text: "This entry was inserted after the view was rendered"
        }));
    }
});

var view = new EntryListView({
    collection: entries,
    el: '#entries'
});
view.render();

entries.fetch({reset: true});

And a demo http://jsbin.com/opodib/1/edit

Upvotes: 4

Related Questions