Here's an example of my collection view:
mod.AppListView = Backbone.View.extend({
initialize: function() {
var self = this
mod.collection.bind('add', self.addOne);
mod.collection.bind('reset', self.addAll);
_.bindAll(self, 'addOne', 'addAll');
this.addAll();
},
events: {
},
addOne: function(myModel) {
var view = new ListItemView({
model: myModel
});
},
addAll: function() {
mod.collection.each(this.addOne);
},
});
On initial run this works fine. But on subsequent resets addAll's this becomes the collection instead of the view, and therefore addOne wouldn't work.
To fix this I had to do:
mod.collection.bind('reset', self.addAll, this);
But I thought that was the point of _.bindAll? Shouldn't that have set the this to be the view? Could this be explained? Is there a way to always ensure this points to the view and not the collection?
Thanks.
_.bindAll must come before any reference to the method. You have it backwards.
_.bindAll(self, 'addOne', 'addAll');
mod.collection.bind('add', self.addOne);
mod.collection.bind('reset', self.addAll);
When you call _.bindAll, it replaces the methods that you've specified with one that has been wrapped / proxied / decorated, to ensure the context is always set correctly. Since the method is being replaced, any reference that you make to the method must be done after the replacement happens. Otherwise the reference will be pointing to the original method and the _.bindAll will appear to have not worked.
As far as _.bindAll vs the 3rd parameter... pick the one you like. I prefer passing in the 3rd parameter when calling .bind, but that's just me. There are cases when I have to use _.bindAll, though. Both of them do the same thing, they just do it in a different way.
Related
I have a context problem / design problem for my Backbone view.
Goal
The user selects a user from a list / user collection in a separate view.
The mentioned view passes an global event that the editUserView receives ("edit-contact").
The editUserView should receive this event and extract the (user) model.id attribute. By using this model.id I want to update the view with the corresponding object retrieved from the existing view model Tsms.Collection.Users.
Problem
The context passed to the updateView function is wrong, and thus I do not have access to the parent views .render() function. The debugger states "render() is not a function".
Since the context is not that of the parent view I am also unable to set the this.current variable.
How would I go about solving this problem?
View code
Tsms.Views.editUserView = Backbone.View.extend({
model: Tsms.Collections.Users,
initialize: function(options) {
Tsms.require_template('edituser')
this.template = _.template($('#template_edituser').html());
this.current = -1;
Tsms.vent.on('edit-contact', this.updateView)
},
updateView: function(model) {
this.current = model.id;
this.render();
},
render: function() {
this.$el.html(this.template(this.model.get(this.current).attributes));
return this;
}
});
Backbone's on actually takes three arguments:
on object.on(event, callback, [context])
[...]
To supply a context value for this when the callback is invoked, pass the optional last argument: model.on('change', this.render, this) or model.on({change: this.render}, this).
The easiest and (currently) most idiomatic way to solve your problem would be to use the third context argument:
Tsms.vent.on('edit-contact', this.updateView, this);
While mu is too short is right, you should use Backbone's listenTo to avoid memory leaks (zombie views).
this.listenTo(Tsms.vent, 'edit-contact', this.updateView);
The context is automatically set to this, the calling view.
When remove is called on the view, stopListening is called and any references kept for events are removed.
Another reason to avoid on is that it should be the view that is responsible for the events it wants to handle, the event bus shouldn't have to know.
I think I am missing something very trivial here. I have created a Backbone View as follows(without extending Backbone.View):
var PlayersView = new Backbone.View({
initialize: function() {
this.render();
},
render: function() {
console.log("hello World");
}
});
But it doesn't log anything when I run this code. It doesn't work when I explicitly do: PlayersView.render(); as well.
But the following code works :
var PlayersView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function() {
console.log("hello World");
}
});
var playersview = new PlayersView();
The View constructor does not accept properties to add to the constructed object. It accepts a few special options like 'model', 'tagName', and so on. But the initialize(...) and render(...) properties in your first code snippet are effectively ignored.
The proper way to provide initialize, render, is to use Backbone.View.extend({...}).
From http://backbonejs.org/#View-extend
extend
Backbone.View.extend(properties, [classProperties]) Get started with
views by creating a custom view class. You'll want to override the
render function, specify your declarative events, and perhaps the
tagName, className, or id of the View's root element.
In other words, your first view's render method was not overridden by your custom render/initialize function.
When using extend you actually let Backbone understand you wish to use your own methods instead of the "default" ones.
I'm using Backbone and I have the following model and collection
App.Models.Person = Backbone.Model.extend({
});
App.Collections.People = Backbone.Collection.extend({
model: App.Models.Person,
url: 'api/people',
});
However what I'm struggling on is the best way to render this collection. Here's how I've done it so far which works but doesn't seem to be the most elegant solution
App.Views.PeopleView = Backbone.View.extend({
el: $(".people"),
initialize: function () {
this.collection = new App.Collections.People();
//This seems like a bad way to do it?
var that = this;
this.collection.fetch({success: function(){
that.render();
}});
},
render: function () {
var that = this;
_.each(this.collection.models, function (item) {
that.renderPerson(item);
}, this);
},
I'm fairly new to Backbone but have to assign this to a different variable to I use it inside of the success function just seems like a bad way of doing things? Any help on best practices would be appreciated.
Backbone allows you to register for events that you can react to. When the collection is synchronized with the server, it will always fire the sync event. You can choose to listen for that event and call any given method. For instance ...
initialize: function () {
this.collection = new App.Collections.People();
this.listenTo(this.collection, "sync", this.render);
// Fetch the initial state of the collection
this.collection.fetch();
}
... will set up your collection so that it would always call this.render() whenever sync occurs.
The docs on Backbone Events are succinct but pretty good. Keep in mind a few things:
The method you use to register event listeners (i.e. listenTo or on) changes how you provide the context of the called function. listenTo, for instance, will use the current context automatically; on will not. This piece of the docs explains it pretty well.
If you need to remove a view, you will need to disconnect event listeners. The easiest way to do that is to use listenTo to connect them in the first place; then when destroying the view you can just call view.stopListening().
For rendering, there are a lots of suggestions for how to do it. Generally having a view to render each individual model is one way. You can also use Backbone.Collection#each to iterate over the models and control the scope of the iterating function. For instance:
render: function() {
this.collection.each(function(model) {
var view = new App.Collections.PersonView({ model: model });
view.render();
this.$el.append(view.$el);
}, this);
}
Note the second argument to .each specifies the scope of the iterator. (Again, have a look at the docs on controlling scope. If you'd rather have a framework help out with the rendering, check out Marionette's CollectionView and ItemView.
If your view is supposed to just render the collection you can send the collection to temnplate and iterate through in template, otherwise you can create another subView for that purpose or send the individual models of the collection to another subview and append to the container, hope it was helpful.
I am learning backbone.js following this tutorial, but I run into problem understanding the first example:
(function($){
var ListView = Backbone.View.extend({
...
initialize: function(){
_.bindAll(this, 'render'); // fixes loss of context for 'this' within methods
this.render(); // not all views are self-rendering. This one is.
},
...
});
...
})(jQuery);
Q1: Why use (function($){})(jQuery); instead of a perfectly fine working (function(){})();?
Q2: What does _.bindAll(this, 'render') do? How does it fixes loss of context for 'this' within method?
Q1: by passing jquery in as a parameter you allow yourself 2 things:
if the need of using 2 versions of jquery arises - you are prepared
module pattern is probably better thought of as something well encapsulated and with well defined dependencies, so by declaring that jquery is a parameter - you declare clear dependency. Granted there are other ways of doing it (like RequireJS), but this is also a way
Q2: bindAll is a utility method from Underscore.js that binds this for a specific method - thus, when that method is invoked (as a callback for instance) the correct this would be used inside of it.
For example:
(function($){
var ListView = Backbone.View.extend({
...
initialize: function(){
// fixes loss of context for 'this' within methods
_.bindAll(this, 'render', 'makestuff');
this.render(); // not all views are self-rendering. This one is.
},
...
makestuff : function() {
...
this.stuff = ... // some action on the list's instance
}
});
...
})(jQuery);
and in some part of your code you doing something like:
var list = new ListView({...});
$('#button').on('click', list.makestuff);
this in makestuff method is a reference to the above list and not whatever context the on function is in when makestuff is actually invoked inside it.
The actual implementation relies on using apply and call functions to bind the context of function's execution to a specific object.
(function($){})(jQuery) passes jQuery to the self executing which is using it as $. This makes sure you can safely use the $ symbol inside the closure without having to worry about interference with other libraries or even other versions of jQuery. A common example for this practice would also be passing in window and document and then using shorthands inside the closure:
(function(w, d, $){
$(w).resize(function({}); //equals $(window) now
})(window, document, jQuery);
underscore's _.bindAll does the following:
Binds a number of methods on the object, specified by methodNames, to
be run in the context of that object whenever they are invoked. Very
handy for binding functions that are going to be used as event
handlers, which would otherwise be invoked with a fairly useless this.
If no methodNames are provided, all of the object's function
properties will be bound to it.
See the annotated source for the how.
I have this backbone that is working fine, i just want to render the collection is been fetched!
code:
var SearchView = Backbone.View.extend({
events: {
"keyup": "handleChange"
},
initialize: function(){
this.collection.bind("reset", this.updateView, this);
},
render: function(){
// this is where i need help
this.$el.next('#suggestions').append(// iterate over this.collection and apppend here) //
},
handleChange: function(){
var term = this.$el.val();
this.collection.search(term);
},
updateView: function() {
this.render();
}
});
I just want to iterate over this.collection and display the "text" attribute thats inside each collection and append it to the ('#suggestions') el. thanks
Focusing on your reset, render over collections issue, this is how I would do it. This way assumes that when you create your view, the $el is already present and you're passing it in through the View constructor so it's ready to go.
var SearchView = Backbone.View.extend({
template: _.template('<span><%= term %></span>');
initialize: function(){
this.collection.bind("reset", this.render, this);
},
render: function(){
this.addAllTerms();
return this;
},
addAllTerms: function() {
var self = this;
this.collection.each(function(model) {
self.addTerm(model);
});
},
addTerm: function(someModel) {
this.$el.next('#suggestions').append(this.template(someModel.toJSON()));
}
});
It's a bit different from your approach in a few ways. First, we utilize Underscore's template function. This could be anything from span to li to div whatever. We use the <%= %> to indicate that we're going to interpolate some value (which will come from our model.term attribute).
Instead of going to the handler then render, I just bind the collection to render.
The render() assumes we're always going to refresh the whole thing, build from scratch. addAllTerms() simply cycles through the collection. You can use forEach() or just each() which is the same thing except forEach() is 3 characters too long for my lazy bum. ;-)
Finally, the addTerm() function takes a model and uses it for the value that it will append to the #suggestions. Basically, we're saying "append the template with interpolated value". We defined the template function up above as this View object property to clearly separate the template from data. Although you could have skipped this part and just append('<span>' + someModel.get('term') + '</span>') or what not. The _.template() uses our template, and also takes any sort of object with the property that lines up with the one in our template. In this case, 'term'.
It's just a different way to do what you're doing. I think this code is a little more manageable. For example, maybe you want to add a new term without refreshing the whole collection. The addTerm() function can stand on its own.
EDIT:
Not that important but something I utilize with templates that I found useful and I didn't see it when I first started out. When you're passing the model.toJSON() into the template function, we're essentially passing all the model attributes in. So if the model is like this:
defaults: {
term: 'someTerm',
timestamp: '12345'
}
In our previous example, the attribute timestamp is also passed in. It isn't used, only <%= term %> is used. But we could easily interpolate it as well by adding it to the template. What I want to get at is that you don't have to limit yourself to data from one model. A complex template might have data from several models.
I don't know if it's the best way, but what I do is have something like this:
makeHash: function() {
var hash = {};
hash.term = this.model.get('term');
hash.category = anotherModel.get('category');
var date = new Date();
hash.dateAccessed = date.getTime();
return hash;
}
So you can easily build your own custom hash to throw into a template, aggregating all the data you want to interpolate into a single object to be passed into a template.
// Instead of .toJSON() we just pass in the `makeHash()` function that returns
// a customized data object
this.$el.next('#suggestions').append(this.template(this.makeHash()));
You can also easily pass in whole objects.
makeHash: function() {
var hash = {};
hash.term = this.model.get('term');
var animal = {
name: 'aardvark',
numLegs: 4
};
hash.animal = animal;
return hash;
}
And pass this into our template that looks like this:
template: _.template('<span><%= term %></span>
<span><%= animal.name %></span>
<span><%= animal.numLegs %></span>')
I'm not sure if this is the best way but it helped me understand exactly what data is going into my templates. Maybe obvious but it wasn't for me when I was starting out.
I found the solution to the problem, im gonna put it here for people that might want to know:
render: function(){
this.collection.forEach(function(item){
alert(item.get("text"));
});
}