Search code examples
jquerybackbone.jsbackbone-events

backbonejs remove anomaly concept


I was building a small app for adding and deleting li from ul using Backbonejs.One of the SO members cymen helped me code it, using that i tailored the code a little.currently if i add one element and delete , it works , but the second time i add an element (to ul) and go to delete it , i get

Uncaught TypeError: Cannot call method 'remove' of undefined

Pasting my code here ,

HTML :

<input type="text" id="name">
<button id="add">Add</button>
<ul id="mylist"></ul>

JS:

$(function(){

        var myCollection = Backbone.Collection.extend();

        var myView = Backbone.View.extend({

            el:$('body'),

            tagName:'li',

            initialize : function(e){
                this.collection.bind("add",this.render,this);
                this.collection.bind("remove",this.render,this);
            },

            events:{
                'click #add' : 'addfoo'                
            },

            addfoo : function(){
               var myname= $('#name').val();
               $('#name').val('');               
               this.collection.add({name:myname});
            },

            render : function(){

                $('#mylist').empty();
                this.collection.each(function(model){
                    console.log("myView");
                    var remove = new myRemoveView({model:model});
                    remove.render();
                });
            }
        });

        var myRemoveView = Backbone.View.extend({

        el:$('body'),

        events:{
            'click .button':'removeFoo'
        },

        removeFoo : function(){
            console.log("here");

            this.model.collection.remove(this.model);
        },

        render : function(){
            console.log("second view");
            $('#mylist').append('<li>'+this.model.get('name') + "<button class='button'>"+"delete"+"</button></li>");
            return;
        }


        });
        var view = new myView({collection: new myCollection()});
            });

Two things i did not understand :

i) in the removeFoo function , we write

this.model.collection.remove(this.model)

shouldnt this have been this.collection.model.remove , something of that sort ?

ii) i add a li to ul , then i delete it , when i add another li (appending to ul works perfect) but this time when i go to delete it throws me the above error : Uncaught TypeError :cannot call method 'remove' of undefined

can you please help me figure out these 2 doubts in my code , btw SO member cymen's code works like a charm only my tailored code (above) is giving me errors.

SO member cymen's code : JS Fiddle for his code

Thank you


Solution

  • First of all, your myRemoveView is using <body> as its el:

    var myRemoveView = Backbone.View.extend({
        el:$('body'),
    

    That means that every time you hit the delete button, you're going to trigger removeView on every single myRemoveView you've made. That's certainly not what you want to happen and part of the reason that cymen is using tagName: 'li'. There are two general rules for views and their els:

    1. One view per el and one el per view. You don't want views sharing the same el because of the event handling problem; doubly so you don't want several instances of the same view sharing an el.
    2. The view's el should be the part of the DOM that the view cares about, no more and no less. This makes cleaning up quite easy, just delete the DOM element associated with your view and most things go away with it (DOM events in particular); for non-DOM events, we have a remove method on views.

    Another thing, your view shouldn't (generally) be messing with the DOM outside it's own el. This looks very odd:

    render : function(){
        $('#mylist').append(...);
        return;
    }
    

    The caller should be responsible for figuring out where your el goes, your view should only concern itself with things happen inside it, some other view should be responsible for #mylist. Also, render methods conventionally return this so that you can do this:

    $(x).append(some_view.render().el);
    

    to put them into the DOM.

    Specifying both tagName and el in a view:

    var myView = Backbone.View.extend({
        el: $('body'),
        tagName: 'li',
    

    is pointless, the tagName will be ignored and el will be used.

    You're also using Backbone 0.5.3 in your fiddle. You should be using the latest versions whenever possible.

    If we correct the above, then everything starts working (again):

    var myView = Backbone.View.extend({
        //...
        render: function() {
            $('#mylist').empty();
            this.collection.each(function(model) {
                var remove = new myRemoveView({
                    model: model
                });
                $('#mylist').append(remove.render().el);
            });
        }
        //...
    });
    

    and:

    var myRemoveView = Backbone.View.extend({
        tagName: 'li',
        //...
        render: function() {
            this.$el.text(this.model.get('name'));
            this.$el.append("<button class='button'>delete</button>");
            return this;
        }
    });
    

    Demo: http://jsfiddle.net/ambiguous/2z4SA/1/


    So what sort of craziness was going on with your original version? The key is el: $('body') in your myRemoveView. First we'll add a little logging method to myRemoveView to make it easier to watch what happens:

    _log: function(method) {
        console.log(
            method,
            ': model =', this.model.cid,
            ' got-collection =', this.model.collection ? 'yes' : 'no',
            ' view =', this.cid
        );
    }
    

    Note that cid is an internal unique ID that Backbone creates, it is just a convenient way to keep track of things. Then we'll call this._log in removeFoo and render:

    removeFoo: function() {
        this._log('removeFoo');
        if(this.model.collection)
            this.model.collection.remove(this.model);
    },
    render: function() {
        this._log('render');
        $('#mylist').append('<li>' + this.model.get('name') + "<button class='button'>" + "delete" + "</button></li>");
        return;
    }
    

    Here's a simple process that should show you were everything goes wrong:

    1. Add a to the list.
    2. Add b to the list.
    3. Hit a's delete button.

    You can follow along here: http://jsfiddle.net/ambiguous/yLYNL/

    First we'll add a and this pops up in the console:

    render : model = c1  got-collection = yes  view = view2
    

    Then we add b and see this:

    render : model = c1  got-collection = yes  view = view4
    render : model = c3  got-collection = yes  view = view5
    

    Your myView render redraws the whole collection so we see c1 for a and c3 for the new b. So far so good, everything makes sense.

    Now, when we'll try to delete a; first we see that we removeFoo on a:

    removeFoo : model = c1  got-collection = yes  view = view2
    

    That will trigger a myView#render which redraws the whole collection. The whole collection is just b at this point so we see c3 rendered again and everything still makes sense:

    render : model = c3  got-collection = yes  view = view6
    

    But now we see everything go sideways:

    removeFoo : model = c1  got-collection = no   view = view4
    removeFoo : model = c3  got-collection = yes  view = view5 
    

    You'll see c3 show up because you have two myRemoveView instances (one for a and one for b) bound to the same el so both of them will see the click .delete event, it just so happens that c1 sees it first.

    But what is that c1 doing there? That's the one that doesn't have a collection, that's the the one that is triggering your original error. You never detach your myRemoveViews from events on <body> so you have a zombie: even though the <li> is gone, the view is still bound to <body> through the view's delegate call. So you have a zombie view that references a zombie model, zombies zombies everywhere and you left your zombie fighting kit in the car. But why doesn't c1 have a collection? Well, you did :

    this.model.collection.remove(this.model)
    

    on c1 to remove it from the collection; removing a model from a collection removes the model's collection because, well, the model is no longer in a collection.