Question

This is my first attempt at using Backbone.js, so I decided to make a simple test application that simulates a restaurant menu with menu items. I was following along with this cool blog post from andyet.net.

I'm having a problem when adding a model to a collection. I have bound a view method to the collection's add event that should update the view. Here is the code with as much irrelevant code removed as possible.

(note: I removed things like local scope var declarations and closures for the sake of brevity. The following is still a little long, and I know that's annoying, but it should be pretty straightforward and easy to understand):

MenuItem = Backbone.Model.extend({
    // initialize functions removed for brevity
});
Menu = Backbone.Model.extend({
    // initialize functions removed for brevity
    // MenuSelection property is defined here.
});
MenuSelection = Backbone.Collection.extend({ model: MenuItem });
MenuItemView = Backbone.View.extend({
    // render template
});

/**
 * This is unaltered from the code.  The error occurs here.
 */
RestaurantAppView = Backbone.View.extend({
    addMenuItem : function (MenuItem) {
        var view = new MenuItemView({ model : MenuItem });
        this.menuList.append(view.render().el);
        // ERROR occurs here.  error: "this.menuList is undefined"
    },
    initialize : function () {
        this.model.MenuSelection.bind('add', this.addMenuItem);
    },
    render : function () {
        $(this.el).html(ich.app(this.model.toJSON()));
        this.menuList = this.$('#menuList'); // IT IS DEFINED HERE
        return this;
    }
});

/**
 * Everything after this I left in just-in-case you need to see it.
 */
RestaurantAppController = {
    init : function (spec) {
        this.config = { connect : true };
        _.extend(this.config, spec);
        this.model = new Menu({
           name : this.config.name,
        });
        this.view = new RestaurantAppView({ model : this.model });
        return this;
    }
};
$(function() {
    // simulating ajax JSON response.
    var json = {
        Menu : {
            name : 'Appetizers',
            MenuItem : [
                {
                    name : 'toast',
                    description : 'very taosty',
                    price : '$20.00'
                },
                {
                    name : 'jam',
                    description : 'very jammy',
                    price : '$10.00'
                },
                {
                    name : 'butter',
                    description : 'very buttery',
                    price : '$26.00'
                }
            ]
        }
    };
    window.app = RestaurantAppController.init({
        name : json.Menu.name
    });
    $('body').append(app.view.render().el);
    app.model.MenuSelection.add(json.Menu.MenuItem);
});  

I've marked with comments the problematic area. According the the Backbone Documentation:

If jQuery or Zepto is included on the page, each view has a $ function that runs queries scoped within the view's element.

So, if I'm setting this.menuList = this.$('#menuList'); in the render method, why can I not access this.menuList in the addMenuItem method? The demo I linked to at the top does it exacly like this. Also, if I swap out this.menuList for a jQuery selector, like so:

addMenuItem : function (MenuItem) {
    var view = new MenuItemView({ model : MenuItem });
    $('#menuList').append(view.render().el);
}

Everything works fine. But I don't want to re-select the menulist ID every time addMenuItem is executed. The RightWayTM is to cache it after it is rendered.

Also note: I thought maybe the problem was with the ICanHaz template not returning fast enough, but then this.menuList would be an empty array, not undefined, so that's not it.

Was it helpful?

Solution

You're running into the #1 gotcha with JavaScript -- the dynamically scoped this keyword. When you pull off this.addMenuItem as a reference without binding it, the addMenuItem function loses its notion of this. There are two easy ways to fix it in your code, either replace this line:

this.model.MenuSelection.bind('add', this.addMenuItem);

With this:

this.model.MenuSelection.bind('add', _.bind(this.addMenuItem, this));

Or add this line to the top of your initialize function, which will accomplish effectively the same thing:

_.bindAll(this, 'addMenuItem');

OTHER TIPS

Probably addMenuItem method is being called before render method is called and hence the definition for menuList is missing in the addMenuItem.

Why don't you push this.menuList = this.$('#menuList'); to initialize method and try?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top