why do bindAll in backbone.js views? - backbone.js

In backbone's todo demo the code has a few spots where _.bindAll(this,...) is used. Specifically it's used in the initialize function of both views. As far as I can tell it's necessary to do the following:
this.$('.todo-content').text(content);
But why would one want to do the above, when one can do:
$('.todo-content').text(content);
?

_.bindAll( this, ... ) is necessary not only for this.$( selector ).doSomething() but generally to be sure that this in your view's method is always pointing to the view itself.
For example, if we want to refresh our view when the model changes, we bind the view's render method to the model's change event:
initialize: function() {
this.model.bind( 'change', this.render );
},
Without _.bindAll( this, 'render' ), when the model changes this in render will be pointing to the model, not to the view, so we won't have neither this.el nor this.$ or any other view's properties available.

As of Backbone 0.5.2, it's no longer necessary to use _.bindAll(this...) in your views to set the context of the "bind" callback functions, as you can now pass a 3rd argument to bind() that will set the context (i.e. "this") of the callback.
For example:
var MyView = Backbone.View.extend({
initialize: function(){
this.model.bind('change', this.render, this);
},
render: function(){
// "this" is correctly set to the instance of MyView
}
});

this.$ limits jQuery's context to the view's element, so operations are quicker.
Additionaly, this.$('.todo-item') won't find your elements with todo-item class outside your view's element.

Related

Backbone subviews events not binded correctly [duplicate]

I am working on a Backbone application which is based on a dynamic template . I have a header view, a side panel view and footer view that are dynamically initialized when calling any other view .
The problem is I have events on each template view that are not firing. For example i have a button that changes the language in the header view but its event isn't firing.
My header View :
define([ "jquery", "backbone", "text!../../pages/header.html" ], function($,
Backbone, headerTpl) {
var header = Backbone.View.extend({
events : {
"click #enBtn":"swichToEnglish",
"click #frBtn":"swichToFrench"
},
initialize : function(options) {
_.bindAll(this, "swichToFrench","swichToEnglish");
this.render(options.parent);
//$("#enBtn").css("pointer-events","none");
},
render : function(parent) {
this.template = _.template(headerTpl);
$(parent).append(this.template);
return this;
},
swichToFrench:function(){
console.log("switch to frensh");
if(i18n.currentLocal == 'en'){
i18n.currentLocal='fr';
$("#frBtn").css("pointer-events","auto");
this.render(options.parent);
}
},
swichToEnglish:function(){
console.log("switch to English");
if(i18n.currentLocal == 'fr'){
i18n.currentLocal='en';
$("#enBtn").css("pointer-events","auto");
$("#frBtn").css("pointer-events","none");
this.render(options.parent);
}
}
});
return header;
});
The header view is called in the router :
self._header = new Header({parent: $(".main_container")});
Any ideas how to fix this issue. I need to know haw to fire these events Thank You.
The reason your event handlers is not firing is because the event handlers are delegated to the views element, but you're appending the template to some other element. Since the target elements are in this template which is not appended the view's el, the events will never bubble into the handlers delegated to it.
Apart from that, as #mu is too short pointed out, when you do $(parent).append(this.template);,
this.template is the template function. You should actually call it with the data to get the template.
and you shouldn't be using global selectors like $('') and use this.$el.find('') instead as best practice.
also, options is only available inside the initialize method, and is undefined outside.
Instead of passing the parent into the view and then have it append itself to parent, do that outside the view and make the view independent.
Also declare the template property in the view rather than adding it after the creation as a best practice.
And there's no need to bind the context of event handlers to the view manually, by default the context of event handler will be the view.
Your view should look like:
define([ "jquery", "backbone", "text!../../pages/header.html" ], function($,
Backbone, headerTpl) {
var header = Backbone.View.extend({
initialize : function(options) {
this.render();
},
template: _.template(headerTpl),
events : {
"click #enBtn":"swichToEnglish",
"click #frBtn":"swichToFrench"
},
render : function() {
this.$el.append(this.template(/* pass the data here*/));
//--------^----- this is required for the events to work
return this;
},
swichToFrench:function(){
if(i18n.currentLocal == 'en'){
i18n.currentLocal='fr';
this.$el.find("#frBtn").css("pointer-events","auto");
this.render();
}
},
swichToEnglish:function(){
if(i18n.currentLocal == 'fr'){
i18n.currentLocal='en';
this.$el.find("#enBtn").css("pointer-events","auto");
this.$el.find("#frBtn").css("pointer-events","none");
this.render();
}
}
});
return header;
});
Which you can create like:
self._header = new Header();
$(".main_container").append(self._header.el);
it looks like you just want the view content to be added to '.main_container', and doesn't need another element. In that case you can make your views el point to it rather than creating a new element by passing it as el in the options like:
self._header = new Header({
el: '.main_container' // this.el will refer to `.main_container` in view
});
then you don't have to do $(".main_container").append(self._header.el);
if you must pass the parent into view as an options for some reason, then you should cache it in the view inside initialize so that you can refer it elsewhere like.
this.parent = options.parent
side note:
As you can see, I've changed the order in which you had declared the view's properties - initialize on top followed by template, event and render.
We initialize the view, we create the templating function, we declare the events to be delegated, and then we render the view.
The order in which you define properties doesn't matter internally, but when another developer looks at your code, it's much easier to digest. But it's a matter of opinion.

backbone js view event binding only to views elements

Hi I'm learning backbone and I am having trouble with binding events to views. My problem is that I have a view constructor that when called, binds all views to a button press event that is only part of one view. I would like the button press event to be bound to only the 1 view that contains the button.
http://jsbin.com/tunazatu/6/edit?js,console,output
click on all of the view buttons
then click back to view 1
click the red button (all view's models console.log their names)
So I've looked at the code from this post mutliple event firing which shows that you can have multiple views that have the same el thru tagName but map events only to their html elements. This is also what is done in the localtodos example from Jérôme Gravel-Niquet
I have also tried not declaring el /tunazatu/7/edit?js,console,output but then it seems like no event gets bound.
var AppView = Backbone.View.extend({
tagName:"div", //tagName defined
getName:function(){
console.log(this.model.get('name'));
},
initialize:function(options){
this.listenTo(this.model, 'change', this.render);
var temp_mapper = {appView1:'#route1',appView2:'#route2',appView3:'#route3'};
var m_name = this.model.get('name');
this.template = _.template($(temp_mapper[m_name]).html()); //choose the correct template
},
render:function(){
var temp = this.template(this.model.toJSON()); //populate the template with model data
var newElement = this.$el.html(temp); //put it in the view's tagName
$('#content').html(newElement);
},
events:{
"click button":"log"
},
log:function(){
this.getName();
}
});
Your problem is that your AppView really looks like this:
var AppView = Backbone.View.extend({
el: "#content",
//...
Every time you create a new AppView, you bind another event delegator to #content but you never remove those delegations. If you create three AppViews, you end up with three views listening to click button inside #content.
I would recommend two things:
Avoid trying to re-use views, create and destroy them (via View#remove) as needed. Views should be lightweight enough that putting them together and tearing them down should be cheap.
Don't bind multiple views to the same el. Instead, let each view create its own el and then let the caller put that el inside some container.
If you do both of those things then your problem will go away. Your AppView would look more like this:
var AppView = Backbone.View.extend({
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this; // Common practise, you'll see why shortly.
},
// As you already have things...
});
Then your router methods would look more like this:
view1: function() {
if(this.appView)
this.appView.remove();
this.appView = this.createView('appView1');
$('#content').html(this.appView.render().el);
// that `return this` is handy ----------^^
},
If you must stick with your current approach then you'll have to call undelegateEvents on the current AppView before you render another one and delegateEvents on the new AppView after you render it.
But really, don't be afraid to destroy views that you don't need right at this moment: destroy any view that you don't need on the page right now and create new instances when you need them. There are cases where you don't want to destroy your views but you can usually avoid it.

How can I listenTo a NestedModel change on it's collection?

I'm using NestedModel change plugin to support nested model changes into my models.
I tried this into my View where I got a reference to a collection.
....
initialize: function(){
this.listenTo(this.collection, 'change', foo);
},
foo: function(model){
console.log("Some model changed")
}
By doing FooModel.set({"fooPropertie.BarPropertie": "fooBarValue"});
I would expect to trigger a collection change event.
Am i doing something wrong?
It is hard to define the trouble since you do not provide the way this.collection was defined. But I see at least one thing to tune: this.listenTo(this.collection, 'change', this.foo);. Add this. before foo call. Currently in your context foo is undefined.

Backbone js bind method on collection

I am following the tutorial at:
http://arturadib.com/hello-backbonejs/docs/3.html
This is a piece of code I am stuck on:
initialize: function(){
_.bindAll(this, 'render', 'addItem', 'appendItem'); // remember: 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();
},
The line of code that I am having a hard time understanding is:
this.collection.bind('add', this.appendItem);
I know there is a bind method in underscore, but I don't think that is the same bind function.
Could you explain what the above line is for and where I can read more about it?
In backbonejs, a collection can trigger events. Example:
this.collection.trigger('myEvent');
In addition, you can also bind a collection to some events. Example:
this.collection.bind('myEvent', function() { ... });
Backbone.Collection.bind() method comes from Backbone.Events. Note that Backbone.Collection has all the methods of Backbone.Events mixed in (like all the other backbone.js objects including Backbone itself).
Backbone.Events.bind() is an alias for Backbone.Events.on().

Is it possible to trigger an event when initializing a model?

In a backbone model, is it possible to trigger an event in the initialize function, for a nested view? I based my current code off this example: https://stackoverflow.com/a/8523075/2345124 and have updated it for backbone 1.0.0. Here is my initialize function, for a Model:
var Edit = Backbone.Model.extend({
initialize: function() {
this.trigger('marquee:add');
this.on('change', function(){
this.trigger('marquee:add');
});
}
...
}
I'm trying to call a method renderMarquee when the model is initialized:
var EditRow = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, "change", this.render); // works
this.listenTo(this.model, "marquee:add", this.renderMarquee); // only called when changed, but not when initially created
...
}
renderMarquee IS called when the model is changed, but not when it is initialized. 'change' events work as expected (this.render is called). Any thoughts?
Thanks!
I am currently facing a similar problem. I needed to trigger the change event in the initialize method of my model.
I looked into the backbone code which revealed why this is not happening:
var Model = Backbone.Model = function(attributes, options) {
...
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
};
the set is executed before the initialize and this.change is emptied setting the model state to "nothing has changed".
In order to overwrite behavior this I added the following code to my initialize method.
initialize: function(attributes, options) {
...
this.changed = attributes;
this.trigger('change');
for (attr_name in attributes) {
this.trigger('change:' + attr_name);
}
},
I trigger all change events manually, this is important for me since inheriting models may bind to change or change:attrxy. But this is not enough, because if I just trigger the events the changedAttributes() method would return false therefore I also set this.changed to the current attributes.
This doesn't make a lot of sense because you are initializing the model somewhere prior to doing the view.listenTo call. Unfortunately, you don't really have a choice in that matter.
You are probably going to want to move the event handling to a Backbone.Collection which already has built in events you can listen on for adding/removing.

Resources