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();
Related
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
I am working on a Backbone Project with Backbone.Layoutmanager.js
Ive got a ListView with nested ReceiverViews.
My collection is updated unordered - i want to sort these views BUT i dont want to re-render the whole collection. ( because i loose old data / event handler / graph instance inside old views. )
How to fix ?
ReceiverListView = Backbone.View.extend({
manage:true,
initialize: function(options){
_.bindAll(this, "renderReceiver","renderMe");
this.vent = _.extend({}, Backbone.Events);
this.collection.on('add', this.renderMe, this);
},
renderMe: function(model1){
this.collection.sort(this.collection.comparator);
this.insertView(new ReceiverView({model: model1})).render();
}
You don't need to call sort method manually. Learn about it: http://backbonejs.org/#Collection-sort
initialize: function () {
this.listenTo(this.collection, 'sort', _.bind(this.onSortCollection, this));
},
onSortCollection: function (collection) {
var views = {};
_.each(this.getViews(), function (view) {
if (view.model) views[view.model.cid] = view;
});
collection.each(function (model) {
var view = views[model.cid];
if (view) this.el.appendChild(view.el);
}, this);
}
Hope this helps
I guess that's the simple question. I'm new in js, especially in Backbone.js.
All I want to know is how I can refer to my function inside jquery function.
getLanguages: function() {
...
return languages;
},
render: function() {
...
$("input[type='checkbox']").bind("change", function() {
// todo: getLanguages
});
}
I tried to get languages via this but, of course, I got checkbox in this case.
Edit:
It's so simple. Many thanks to all!!!
This is a classic problem in Javascript. You need to reference this inside a callback, but this changes to the element being bound to. A cheap way to do it:
render: function() {
var that = this;
$("input[type='checkbox']").bind("change", function() {
that.getLanguages();
});
}
that will stay defined as the this that render is defined on.
However, you’re using Backbone, and it has more suitable ways to handle this situation. I don’t know the name of your Backbone.View class, but here’s an example adapted from the documentation:
var DocumentView = Backbone.View.extend({
events: {
"change input[type='checkbox']": "doSomething"
},
doSomething: function() {
this.getLanguages(); # uses the correct this
}
});
Calling bind inside render is not The Backbone Way. Backbone views are made to handle event delegation without the unfortunate need to pass this around.
Save this object before bind change event in the scope of render function.
render: function() {
var CurrentObj = this;
$("input[type='checkbox']").bind("change", function() {
CurrentObj.getLanguages();
});
}
You can save the appropriate object into a local variable so from the event handler, you can still get to it:
getLanguages: function() {
...
return languages;
},
render: function() {
...
var self = this;
$("input[type='checkbox']").bind("change", function() {
var lang = self.getLanguages();
...
});
}
var Text = Backbone.Model.extend({});
Texts = Backbone.Collection.extend({
model: Text,
url: '/data.json',
});
var TextsView = Backbone.View.extend({
initialize: function() {
_.bindAll(this);
this.render();
},
el: "#Texts",
template: _.template($('#TextTemplate').html()),
render: function(e){
_.each(this.model.models, function(Text){
var TextTemplate = this.template(Text.toJSON());
$(this.el).append(TextTemplate);
}, this);
return this;
}
})
var Texts = new Texts();
Texts.fetch();
var TextView = new TextsView({collection: Texts});
this gives me Uncaught TypeError: Cannot read property 'models' of undefined and does not display anything on the page.
This this.model.models should be this.collection
In your render method in your view, you should use this.collection.each instead of _.each function.
render: function(e){
this.collection.each(function(Text){
var TextTemplate = this.template(Text.toJSON());
$(this.el).append(TextTemplate);
}, this);
return this;
}
If you want to use _.each function, then you will need to access the models array directly in your collection as #dfsq pointed out. This can be done by using this.collection.models.
render: function(e){
_.each(this.collection.models, function(Text){
var TextTemplate = this.template(Text.toJSON());
$(this.el).append(TextTemplate);
}, this);
return this;
}
EDIT 2
Here are some reasons your fetch call may not be working. First check that you are using a web server, since ajax requests may be blocked for security reasons using file system. I know this is blocked in Chrome unless you change a certain setting. Not sure about Firefox.
The second reason is that the fetch call is asynchronous. This means that mostly likely your data will not be loaded when you run initialize
This means you'll need to make the following adjustments. First you need to add a listener to the add event of your collection so that anytime an item is added, your view will be notified.
initialize: function() {
_.bindAll(this);
this.render();
// Listen to the `add` event in your collection
this.listenTo(this.collection,"add", this.renderText);
},
Next we need to add a function to your view that will render a single item
renderText: function(Text) {
var TextTemplate = this.template(Text.toJSON());
this.$el.append(TextTemplate);
}
Also to answer your other question about the user of this in the each loop. The last parameter in the each function is the scope you want to use in the inside the callback function that executes. So if you use this as the second parameter, it allows you to access your viewing using this.
this.collection.each(function(Text){
var TextTemplate = this.template(Text.toJSON());
$(this.el).append(TextTemplate);
}, this);
If you don't add this, then you'd need to do this:
var view = this;
this.collection.each(function(Text){
var TextTemplate = view.template(Text.toJSON());
$(view.el).append(TextTemplate);
});
I'm new at Backbone.js.And I hava some problem at this keyworks.I hava a Backbone view blow:
var print = Backbone.View.extend({
el : $('ul li.newItem'),
events : { 'click li.newItem':'printText'},
initialize:function(){
_.bind(printText,this); // does 'this' refer to the li.newItem ?
alert(1233); // init does't work.
},
printText : function(){
//I wanna print the hello world text each list item when clicked.
}
});
var print = new print();
Here is my demo : http://jsbin.com/evoqef/3/edit
You have two problems that are keeping your initialize from working:
There is no printText in scope.
_.bind and _.bindAll behave differently.
The first is easy to fix, you want to use this.printText, not just printText.
_.bind binds a single function to a this and returns that bound function; _.bindAll, on the other hand, binds several named functions to a this and leaves the bound functions attached to the specified this. So, doing this:
_.bind(printText, this);
doesn't do anything useful as you're throwing away the bound function. You'd want to do this:
this.printText = _.bind(this.printText, this);
Or more commonly in Backbone apps, you'd use _.bindAll:
_.bindAll(this, 'printText');
Now you have a functioning initialize and you'll have the right this inside printText and we can move on to fixing printText. I think you want to extract the text from the <li> that was clicked; you can do this like this:
printText: function(ev) {
console.log($(ev.target).text());
}
But that still doesn't work and we're left to wondering what's going on here. Well, Backbone binds events to a view's el so let us have a look at that:
var print = Backbone.View.extend({
el : $('ul li.newItem'),
//...
When that Backbone.View.extend runs, there won't be any li.newItem elements in the DOM so you won't get a useful el in that view. The usual approach here would be to have a view that looks like this:
var Print = Backbone.View.extend({
tagName: 'li',
events: {
'click': 'printText'
},
render: function() {
this.$el.text('Hello world ' + this.options.i);
return this;
},
printText: function(e){
console.log($(e.target).text());
}
});
We set tagName to 'li' and let Backbone create the <li> by itself. Then we'd pass the counter value to the Print view as an argument, Backbone will take care of leaving the argument in this.options.i when we say new Print({ i: ... }).
Now we just have to adjust the addItem method in your ListView to create new Prints and add them to the <ul>:
addItem: function(){
this.counter++;
var v = new Print({ i: this.counter });
this.$('ul').append(v.render().el);
}
Updated demo: http://jsbin.com/evoqef/10/edit
I've also made a few other changes:
Use this.$el instead of $(this.el), there's no need to create something that's already available.
Use this.$() instead of $('ul', this.el), same result but this.$() doesn't hide the context at the end of the $() function call and this.$() is more idiomatic in Backbone.
In your _.bind(printText, this);
The printText is outside of the scope of the init function. this as your second argument represents the print Backbone.View.
You could do something like this:
_.bind(this.printText, this);
and probably rid yourself of the init() error. But you could use this inside of printText and it will represent your View anyway.
printText: function() {
console.log(this); // this represents your view
}