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
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 el
s:
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
.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:
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 myRemoveView
s 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.