I have View listening for an "add" event on a Collection. When the handler fires, the context is the Collection, even though I used _.bindAll() to bind it to the View. Is this a bug, or am I not understanding how this works? jsfiddle
V = Backbone.View.extend({
initialize: function (options) {
this.collection.on('add', this.onAdd);
_.bindAll(this, 'onAdd');
},
onAdd: function () { console.log(this); }
});
c = new Backbone.Collection();
v = new V({collection:c});
c.add(new Backbone.Model());
Outputs:
e.Collection {length: 1, models: Array[1], _byId: Object, _events: Object, on: function…}
Put the bindAll method before the binding to collection statement
This should work:
V = Backbone.View.extend({
initialize: function (options) {
_.bindAll(this, 'onAdd');
this.collection.on('add', this.onAdd);
},
onAdd: function () { console.log(this); }
});
UPDATE:
It's possible not to use the _.bindAll method by applying the context in the .on() method
this.collection.on('add', this.onAdd, this);
Your problem is when your call this.collection.on('add', this.onAdd); first an event is bound to the onAdd function with no context (this = collection at trigger time) and calling _.bindAll(this, 'onAdd'); doesn't override it.
Try to change the order :
_.bindAll(this, 'onAdd');
this.collection.on('add', this.onAdd);
Related
I have this annoying problem and I get the feeling it's because we can't use defaults for Backbone Views as we would with Backbone Models. My goal was to use defaults with a Backbone View and then override them with options passed to the initialize function as desired. The problem that I am having is that when I call this.collection Backbone doesn't match this with this.defaults.collection, as I would expect. I get a null point exception when I call this.collection in the initialize function even though I assigned collection in defaults.
Perhaps what I need is this call in my initialize function:
this.options = _.extend(this.defaults, this.options);
however, in this case, then defaults is not doing anything special. this.defaults could be called this.cholo. I guess I am wondering why defaults/attributes won't act the same as Backbone Models.
I have the following code:
var IndexView = Backbone.View.extend({
el: '#main-div-id',
defaults: function(){
return{
model: null,
collection: collections.users,
childViews:{
childLoginView: null,
childRegisteredUsersView: null
}
}
},
events: {
'click #loginAsGuest': 'onLoginAsGuest',
'click #accountRecoveryId': 'onAccountRecovery'
},
initialize: function (opts) {
this.options = Backbone.setViewOptions(this, opts);
Backbone.assignModelOptions(this,this.options);
_.bindAll(this, 'render', 'onFetchSuccess', 'onFetchFailure');
this.listenTo(this.collection, 'add remove reset', this.render); //this.collection is not defined here
this.collection.fetch({ //null pointer here, this.collection is not defined
success: this.onFetchSuccess.bind(this),
error: this.onFetchFailure.bind(this)
});
},
render: function () {
//removed code because it's extraneous for this example
},
onFetchSuccess: function () {},
onFetchFailure: function () {}
},
{ //classProperties
givenName: '#IndexView'
});
...by the way, in order to make events different for each instance of the view, should I turn events into a function like defaults?
There is really nothing special about the defaults literal within a Backbone.Model. If you take a look at the Backbone source, they're essentially doing this within the model constructor
Backbone.Model = function( attributes, options ) {
// simplified for example
var attrs = _.defaults( {}, attributes, this.defaults );
this.set( attrs, options );
};
You can take the exact same approach when setting up your views
var myView = Backbone.View.extend( {
options: {
// your options
},
initialize: function( options ) {
this.options = _.defaults( {}, options, this.options );
}
} );
If you would like to define your options as a function so it's evaluated at runtime, you can use the following
var myView = Backbone.View.extend( {
options: function() {
// your options
},
initialize: function( options ) {
this.options = _.defaults( {}, options, _.result(this, 'options') );
}
} );
To answer your other question about different events per instance, yes you can either define it as a function and include the logic within that function OR simply pass events: { ... } as an option when instantiating the view.
Hope this helps.
Here is my view:
define(
[
"jquery"
, "underscore"
, "backbone"
, "eventView"
]
, function($, _, Backbone, EventView) {
"use strict";
var TimelineView = Backbone.View.extend({
tagName: 'div'
, className: 'column'
, _EventViews: {} // Cache event views for reuse
, initialize: function() {
this.collection.bind('add', this.add);
this.collection.bind('reset', this.add);
}
, render: function() {
return this;
}
// Listen for additions to collection and draw views
, add: function(model) {
var eventView = new EventView({
model: model
});
// Cache the event
console.log(this._EventViews);
this._EventViews[model.get('id')] = eventView;
// Draw event
eventView.render();
}
});
return TimelineView
}
);
As you can see I set the _EventViews property to contain an empty object. However when I call the add() function console.log(this._EventViews) returns undefined and the following statement fails.
Can anyone tell me why this is?
The problem is that within add, this is not your TimelineView. See this article for an explanation of context in javascript: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/this
You can solve this in a few different ways. The easiest in this situation is to use the third parameter of bind or on (these two are the same).
initialize: function() {
this.collection.on('add', this.add, this);
this.collection.on('reset', this.add, this);
}
Or use listenTo instead.
initialize: function() {
this.listenTo(this.collection, 'add', this.add);
this.listenTo(this.collection, 'reset', this.add);
}
Also, your _EventViews cache will be shared by all instances of TimelineView. If that is not what you want, create it in initialize instead.
initialize: function() {
this._EventViews = {};
this.listenTo(this.collection, 'add', this.add);
this.listenTo(this.collection, 'reset', this.add);
}
It works for me:
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://underscorejs.org/underscore-min.js"></script>
<script src="http://backbonejs.org/backbone.js"></script>
<script>
var TimelineView = Backbone.View.extend({
tagName: 'div'
, className: 'column'
, _EventViews: {} // Cache event views for reuse
, initialize: function() {
//this.collection.bind('add', this.add);
//this.collection.bind('reset', this.add);
}
, render: function() {
return this;
}
// Listen for additions to collection and draw views
, add: function(model) {
var eventView = ({
model: model
});
// Cache the event
console.log(this._EventViews); // Prints: Object {}
this._EventViews[model.get('id')] = eventView;
// Draw event
eventView.render();
}
});
var a = new TimelineView();
a.add();
</script>
I think the problem is the .add() method is invoked from the collection add event. When you add a listener (with in backbone is done with the .bind() function) you must bind (on the native meaning) the function:
_.bindAll(this, 'add');
OR
this.add = this.add.bind(this);
You have to do it before you add the function as a listener:
initialize: function() {
_.bindAll(this, 'add');
this.collection.bind('add', this.add);
this.collection.bind('reset', this.add);
}
Normally one binds model change event to render function like this:
initialize: function() {
this.model.bind('change', this.render, this);
}
How do I bind model change event to render function with parameter:
render: function(templ) {
this.$el.html(templ);
}
initialize: function() {
// ?
}
Something like this?
this.model.bind('change', function() {
return this.render(templ);
}, this);
You can use underscore bind for partial application (that's the term for what you want to do).
this.model.bind('change', _.bind(this.render, this, 'foo', 'bar'));
So render recieves 'foo' and 'bar' as aguments
I need to pass a value to the listView.template in order to be aware the template about the collection.length.
I think one option is to redefine the serializeData method in order to pass a parameter in this way.
var ListView = Marionette.CompositeView.extend({
initialize: function () {
this.collection.on('reset', this.serializeData, this);
this.collection.on('remove', this.serializeData, this);
this.collection.on('add', this.serializeData, this);
},
serializeData: function () {
console.log(this.collection.length);
return {
has_tasks: this.collection.length > 0
};
},
// other codes
});
When I start the app the collection is not yet fetched so:
1.a) the collection.legth = 0
2.b) the template get has_tasks = false as expected.
2.a) after the fetch the collection.length is > 0,
2.b) the serializeData is called and so it puts the has_tasks = true,
2.c) the template seems to be not rendered because it maintains the has_tasks = false
Any idea because 2.c?
Latest Marionette has solved this problem by calling an optional templateHelpers on the view to provide additional context to the view. Also your event binding is not Marionette-friendly as it it will not be auto-unbound correctly when the view is unloaded. So all you need to do in your view is:
var ListView = Marionette.CompositeView.extend({
initialize: function () {
this.bindTo(this.collection, "add", this.render, this);
this.bindTo(this.collection, "remove", this.render, this);
this.bindTo(this.collection, "reset", this.render, this);
},
templateHelpers: function () {
console.log(this.collection.length);
return {
has_tasks: this.collection.length > 0
};
},
// other codes
});
Note, however, that you probably don't want to rerender the entire view and all the sub-elements every time an item is added or removed. A better approach is to only update the count displayed. For instance:
var ListView = Marionette.CompositeView.extend({
initialize: function () {
this.bindTo(this.collection, "add", this.updateCount, this);
this.bindTo(this.collection, "remove", this.updateCount, this);
this.bindTo(this.collection, "reset", this.updateCount, this);
},
updateCount: function() {
this.$('.count_span').html(this.collection.length);
},
// other codes
});
I would simply use something like:
var ListView = Marionette.CompositeView.extend({
initialize: function () {
this.bindTo(this.collection, 'reset', this.render)
},
serializeData: function () {
return { has_tasks: this.collection.length > 0 }
}
});
Calling serializeData again will have no effect on your view. You need to render it again in order to show the new values (because render will get the data by calling serializeData again).
Anyway what is the point of sending hasTask to the template since you can access the collection (and thus its length)?
full code here... http://pastebin.com/EEnm8vi3
line 378 is not inserting the sections into the current view. the section model is correctly being passed into the method. everything else works as expected except for the insertion of the child rendered views.
I am wanting to know why $(this.el) is empty and therefore not allowing an append. trying to use a direct selector like $('#sections') also does not work.
relevent code repeated from pastbin link above: (addOne method)
SCC.Views.SectionView = Backbone.View.extend({
tagName: "div",
className: "section",
initialize: function(){
_.bindAll(this, 'render');
this.template = _.template($('#section-tmpl').html());
},
render: function() {
console.log($(this.el));
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
SCC.Views.SectionsView = Backbone.View.extend({
tagName: "div",
id: "sections",
className: "sections",
initialize: function(){
_.bindAll(this, 'render');
//SCC.Sections.bind('add', this.addOne, this);
SCC.Sections.bind('reset', this.addAll, this);
SCC.Sections.bind('all', this.render, this);
},
render: function() {
$(this.el).html("<p>rendered</p>");
return this;
},
addOne: function(section) {
var view = new SCC.Views.SectionView({model: section});
$(this.el).append(view.render().el);
},
addAll: function() {
this.collection.each(this.addOne);
}
});
SCC.Sections = new SCC.Collections.Sections();
SCC.SectionsView = new SCC.Views.SectionsView({collection:SCC.Sections});
SCC.Sections.reset(window.SectionData);
$('#main').append(SCC.SectionsView.render().el);
I ran into this problem myself and so I'll leave this answer for anyone else out there:
When you bind this.render to 'all' as #lukemh did:
SCC.Sections.bind('all', this.render, this);
You're effectively saying everytime an event is triggered in the model/collection re-render the view. When you use .html() in that render method you're also going to override any child views that may have been .append()'ed to it throught the addOne function.
If you move the $(this.el).html() call to the initialize view the problem is solved. You can still bind render to 'all' but make sure you're only re-rendering a portion of it or else you'll override the child views again.