I'm writing basic to-do list using Backbone.js. Every input adding as a model to collection. Listening for 'add' on collection and rendering newly added model (appending li with 'task' to ul). Then by double-clicking on item I'm retrieving html() of it and in a loop comparing it to corresponding attribute in a model. When it catch the right model - destroying the model (should be deleted from a collection accordingly). But some issue occuring in console, it says
Uncaught TypeError: Cannot read property 'toJSON' of undefined
and adding some buggy effect (not everytime can delete item by the first dblckick). If anyone can point the problem out it would be greatly appreciated!
Here's code
var Model = Backbone.Model.extend({
default: {
task: '',
completed: false
}
});
var Collection = Backbone.Collection.extend({
model: Model
});
var ItemView = Backbone.View.extend({
tagName: 'li',
render: function () {
this.$el.html(this.model.toJSON().task);
return this;
}
});
var TodoView = Backbone.View.extend({
el: '#todo',
initialize: function () {
this.collection = new Collection();
this.collection.on('add', this.render, this);
},
events: {
'click .add': 'add',
'dblclick li': 'destroy',
'keydown': 'keyEvent'
},
add: function () {
this.collection.add(new Model({ //adding input as an model to collection
task: this.$el.find('#todo').val(),
completed: false
}));
this.$el.find('#todo').val(''); //clearing input field
this.$el.find('#todo').focus(); //focusing input after adding task
},
keyEvent: function (e) {
if (e.which === 13) {
this.add();
}
},
destroy: function (e) {
// console.log(this.collection.toJSON());
this.collection.each(function (model) {
if ($(e.target).html() === model.toJSON().task) {
model.destroy();
}
});
e.target.remove();
// console.log(this.collection.toJSON());
},
render: function (newModel) {
var self = this,
todoView;
todoView = new ItemView({
model: newModel
});
self.$el.find('.list').append(todoView.render().el);
return this;
}
});
var trigger = new TodoView();
And here's http://jsbin.com/ciwunizuyi/edit?html,js,output
The problem is that in your destroy method, you find the model to destroy by comparing the task property of the models. If you have multiple models with the same task property, you'll get the error. The actual error occurs because you're removing items from the collection while iterating over it.
Instead of comparing the task property, you could use the cid (client id) property that Backbone gives all models. One way to do this would be this:
When rendering an ItemView, use jQuery's data method to store the cid with the view element (alternatively, use a custom data attribute)
this.$el.data('cid', this.model.cid);
In the destroy function, get the cid property from the view element, and use it to find the right model in the collection (you can use the collection's get method here):
destroy: function (e) {
var id = $(e.target).data('cid');
var model = this.collection.get(id);
model.destroy();
e.target.remove();
},
Adding a unique attribute to the DOM element is only one way to solve this problem. One, much better, alternative would be to listen for the double-click event from the ItemView class itself. That way, you would always have a reference to this.model.
EDIT: This shows the code above in action: http://jsbin.com/nijikidewe/edit?js,output
Related
I have the following View:
var PageView = Backbone.View.extend({
model: Models.MyModel,
initialize: function () {
this.state = window.state;
this.state.on("change", this.render, this);
},
render: function () {
}
});
The state is an another Model that will contain different global settings like: pageNumber, pageSize and etc.
So my question is: is it possible to change this.state.on("change", this.render, this); to something like:
this.state.on("change:pageNumber=2", this.render, this);
i.e. this.render will be executed after state is changed and only if pageNumber property will be equal to 2.
I know that I can just place if statement into render method but if there is way to do that like above it will be greater.
Thanks.
Backbone does not offer a filtering mechanism on events, but you could alter your state model to trigger custom events with the signature you wish.
For example, let's say state is an instance of this class
var EqualBindModel = Backbone.Model.extend({
arm: function(attribute, watchvalue) {
this.on('change:'+attribute, function(model, val, options) {
if (watchvalue === val)
model.trigger('change:'+attribute+'='+val, model, val, options);
});
}
});
you could then setup your custom event with
var state = new EqualBindModel();
state.arm('pageNumber', 2);
and listen to with
state.on("change:pageNumber=2", function(model, value, options) {
console.log('event change:pageNumber=2');
});
And a demo http://jsfiddle.net/ZCab8/1/
Lets Start
devices = new SmartLink.Collections.DeviceMap
view = new SmartLink.Views.DeviceMap({collection: devices})
My Collection
parse: function (response) {
console.log(response.sites);
return response.sites;
}
My View
initialize: function (options) {
this.collection.on('add',this.addOne,this);
this.collection.on('reset',this.addAll,this);
this.collection.fetch();
},
addAll: function() {
console.log("addall");
this.collection.forEach(this.addOne,this);
},
addOne: function(site) {
console.log("addone");
console.log(site);
},
Whats happening?
When parse, it prints out the response as you can see from the array of objects
starts addAll
then addOne them individually
My Question
When I loop through each item, where is all my attributes for each item in the array?.
The only thing I see is the id's.
If I try console.log(site.name), it says undefined.
Use site.get('name') to get the name attribute. You have to use the function get to get attributes from your models.
I had a strange issue working with backbone and binding events. I'll see if I can explain it in a clear way (it's a cropped example...)
In a view, I had the following code in the initialize method
var MyView = Backbone.View.extend({
initialize: function(options) {
//[...]
this.items = [];
this.collection.on('reset', this.updateItems, this);
this.fetched = false;
},
render: function() {
if (!this.fetched) {
this.collection.fetch(); // fetch the collection and fire updateItems
return this;
}
this.$el = $('#my-element');
this.$el.html(this.template(this.items));
},
updateItems: function() {
this.fetched = true;
this.loadItems();
this.render(); // call render with the items array ready to be displayed
}
}
The idea is that I have to fetch the collection, process the items (this.loadItems), and then I set this.$el.
The problem I was facing, is that inside updateItems, I couldn't see any property added after the binding (this.collection.on...)
It seemed like the binding was done against a frozen version of the view. I tried adding properties to test it, but inside updateItems (and inside render if being fired by the collection reset event) I could not see the added properties.
I solved it binding the collection just before fetching it, like this:
render: function() {
if (!this.fetched) {
this.collection.on('reset', this.updateItems, this);
this.collection.fetch();
return this;
}
But it's a strange behavior. Seems like when binding, a copy of 'this' is made, instead of a reference.
Am I right? or there's anything wrong I'm doing?
You should perform your binding in the initialization phase of your collection view:
// View of collection
initialize: function() {
this.model.bind('reset', this.updateItems);
}
now when fetch is finished on the collection updateItems method will be invoked.
Of course you need to bind the model and view before doing this:
var list = new ListModel();
var listView = new ListView({model: list});
list.fetch();
Is there a straightforward way to insert a new model item into the middle of a backbone.js Collection and then update the collection's View to include the new item in the correct position?
I'm working on a control to add/delete items from a list. Each list item has its own Model and View, and I have a View for the entire collection as well.
Each item view has a Duplicate button that clones the item's model and inserts it into the collection at the index position below the item that was clicked.
Inserting the item into the collection was straightforward, but I'm having trouble figuring out how to update the collection view. I've been trying something like this:
ListView = Backbone.View.extend({
el: '#list-rows',
initialize: function () {
_.bindAll(this);
this.collection = new Items();
this.collection.bind('add', this.addItem);
this.render();
},
render: function () {
this.collection.each(this.addItems);
return this;
},
addItem: function (item) {
var itemView = new ItemView({ model: item }),
rendered = itemView.render().el,
index = this.collection.indexOf(item),
rows = $('.item-row');
if (rows.length > 1) {
$(rows[index - 1]).after(rendered);
} else {
this.$el.append(rendered);
}
}
}
This implementation is sort of working, but I'm getting strange bugs when I add a new item. I'm sure I can sort those out, but ...
There's a voice in my head keeps telling me that there's a better way to do this. Having to manually figure out where to insert a new ItemView seems really hacky--shouldn't the collection view know how to rerender the collection already?
Any suggestions?
I don't think re-rendering the whole collection when adding a new element is the best solution. It's slower than inserting the new item at the right place, especially if the list is long.
Also, consider the following scenario. You load a few items in your collections, and then you add n more items (say the user clicks a "load more" button). To do this you would call the fetch() method passing add: true as one of the options. As the data is received back from the server, the 'add' event is fired n times and you'd end up re-rendering your list n times.
I'm actually using a variant of the code in your question, here's my callback to the 'add' event:
var view, prev, prev_index;
view = new ItemView({ model: new_item }).render().el;
prev_index = self.model.indexOf(new_item) - 1;
prev = self.$el.find('li:eq(' + prev_index + ')');
if (prev.length > 0) {
prev.after(view);
} else {
self.$el.prepend(view);
}
So, essentially I'm just using the :eq() jQuery selector instead of getting all the elements as you do, should use less memory.
The usual way I'm doing is let the ListView render each ItemView in its render function. Then I just bind the add event to the render function, like this:
ListView = Backbone.View.extend({
el: '#list-rows'
, initialize: function () {
_.bindAll(this);
this.collection = new Items();
this.collection.bind('add', this.render);
this.render();
}
, render: function () {
this.$el.empty();
var self = this;
this.collection.each(function(item) {
self.$el.append(new ItemView({ model: item }).render().el);
});
return this;
}
}
Everytime you call this.collection.add(someModel, {at: index}), the view will be re-rendered accordingly.
What is the correct way to persist an inherited variable, on action to the parent in Backbone.js?
I can see some logical ways to do this but they seem inefficient and thought it might be worth asking for another opinion.
The two classes are both views which construct a new model to be saved to a collection, the parent passing a variable through to a popup window where this variable can be set.
I'm not sure there's enough detail in your question to answer, but there are a few ways to to do this:
Share a common model. As you describe it, you're using two views to construct a model, so the easiest way is probably to pass the model itself to the child view and have the child view modify the model, rather than passing any variables between views:
var MyModel = Backbone.Model.extend({});
var ParentView = Backbone.View.extend({
// initialize the new model
initialize: function() {
this.model = new MyModel();
},
// open the pop-up on click
events: {
'click #open_popup': 'openPopUp'
},
openPopUp: function() {
// pass the model
new PopUpView({ model: this.model })
}
});
var PopUpView = Backbone.View.extend({
events: {
'change input#someProperty': 'changeProperty'
},
changeProperty: function() {
var value = $('input#someProperty').val();
this.model.set({ someProperty : value });
}
});
Trigger an event on the parent. If for some reason you can't just pass the value via the model, you can just pass a reference to the parent and trigger an event:
var ParentView = Backbone.View.extend({
initialize: function() {
// bind callback to event
this.on('updateProperty', this.updateProperty, this);
},
updateProperty: function(value) {
// do whatever you need to do with the value here
},
// open the pop-up on click
events: {
'click #open_popup': 'openPopUp'
},
openPopUp: function() {
// pass the model
new PopUpView({ parent: this })
}
});
var PopUpView = Backbone.View.extend({
events: {
'change input#someProperty': 'changeProperty'
},
changeProperty: function() {
var value = $('input#someProperty').val();
this.options.parent.trigger('updateProperty', value);
}
});