I'm working on a Backbone application where I allow the user to Add multiple items.
Here's my Model:
//Model
var Item = Backbone.Model.extend({
defaults: {
part1: 'hello',
part2: 'world'
},
validate: function (attr, options) {
var error = '';
//console.log(attr);
if(attr.part2 == "world1"){
this.trigger('err:world1');
error = 'world 1 error';
}
if(attr.part2 == "world3"){
this.trigger('err:world3');
error = 'world 3 error';
}
}
});
Collection:
//Collection
var List = Backbone.Collection.extend({
model: Item,
validateModels: function() {
var cloneCollection = this.clone();
var errorModels = this.filter(function(m) {
if (!m.isValid()) {
return m;
}
});
// cloneCollection.remove(errorModels);
return cloneCollection;
}
});
I allow a user to Add/Delete items from the view as:
//Item View
var ItemView = Backbone.View.extend({
tagName: 'li', // name of tag to be created
events: {
'click span.swap': 'swap',
'click span.delete': 'remove'
},
initialize: function(){
_.bindAll(this, 'render', 'unrender', 'swap', 'remove'); // every function that uses 'this' as the current object should be in here
this.model.bind('change', this.render);
this.model.bind('remove', this.unrender);
this.model.on('err:world1', this.world1Err);
this.model.on('err:world3', this.world3Err);
},
render: function(){
$(this.el).html('<span style="color:black;">'+this.model.get('part1')+' '+this.model.get('part2')+'</span> <span class="swap" style="font-family:sans-serif; color:blue; cursor:pointer;">[swap]</span> <span class="delete" style="cursor:pointer; color:red; font-family:sans-serif;">[delete]</span> <span class="error" style="color:red; font-family:sans-serif;"></span>');
return this; // for chainable calls, like .render().el
},
unrender: function(){
$(this.el).remove();
},
swap: function(){
var swapped = {
part1: this.model.get('part2'),
part2: this.model.get('part1')
};
this.model.set(swapped);
},
remove: function(){
this.model.destroy();
},
world1Err: function(){
alert('World 1 Err');
//console.log(this);
},
world3Err: function(){
alert('World 3 Err');
}
});
//Composite View
var ListView = Backbone.View.extend({
el: $('body'), // el attaches to existing element
events: {
'click button#add': 'addItem',
'click button#save': 'saveCollection'
},
initialize: function(){
_.bindAll(this, 'render', 'addItem', 'appendItem'); // every function that uses 'this' as the current object should be in here
this.collection = new List();
this.collection.bind('add', this.appendItem); // collection event binder
this.counter = 0;
this.render();
},
render: function(){
var self = this;
$(this.el).append("<button id='add'>Add list item</button>");
$(this.el).append("<button id='save'>SAVE</button>");
$(this.el).append("<ul></ul>");
_(this.collection.models).each(function(item){ // in case collection is not empty
self.appendItem(item);
}, this);
},
addItem: function(){
this.counter++;
var item = new Item();
item.set({
part2: item.get('part2') + this.counter // modify item defaults
});
this.collection.add(item);
},
appendItem: function(item){
var itemView = new ItemView({
model: item
});
$('ul', this.el).append(itemView.render().el);
},
saveCollection: function(){
var collectionLength = this.collection.length;
if(collectionLength > 0){
this.collection.validateModels();
//console.log(status);
}
else
alert('Collection is empty. Please add something.');
}
});
Now when a user starts the application, he/she will be presented with the screen:
When user clicks on Add, item would be added like:
I've put in hardcoded validation where 1st and 3rd added element would return error when user clicks on SAVE.
Where I'm stuck is how do I show those error only at that particular item view. For instance, if there's an error at 1st and 3rd item, then the model returns that error but I want to map that error to the 1st and 3rd list-item only, much like this:
Please help me suggest ways to approach it. Thanks in advance!
UPDATE: I've found a fix to that. So whenever there's a validation error, I do something like this:
world1Err: function(){
this.$el.find('span.error').text('Error Occured.')
},
Key things to note:
$(this.el)
, usethis.$el
insteadlistenTo
instead ofon
(bind) to avoid memory leaks (added advantage is that callbacks will be fired with the listener as context, in your case theview
)remove
method ofBackbone.View
unless you know what you're doing and handle all the things it does by yourselfSmart moves:
listenTo
, no need to use_.bindAll
this.collection.each
instead of_(this.collection.models).each
template
method rather than manually generating the templatethis.$(selector)
instead ofthis.$el.find(selector)
,$(selector, this.el)
etcnew Item()
, set it's attributes and then add it to collection, just pass the attributes to collectionsadd
method, it'll create a model instance internallySuggestions:
view.el
rather thanview.render().el
(I really don't know who invented this way or why)You can generalize your code as shown below: