For the following code, the add event bound in the view fires twice (more if you add more elements to the collection at once).
http://jsfiddle.net/radu/GnG66/
App = window.App || {};
var Model = Backbone.Model.extend();
var Collection = Backbone.Collection.extend();
App.collection = new Collection({ model: Model });
var View = Backbone.View.extend({
events: {
'click': function() {
console.log('click');
App.collection.add([{
foo: 'foo'
}, {
bar: 'bar'
}]);
}
},
initialize: function() {
App.collection.on('add', function() {
console.log('Something has been added to the collection')
}, this);
}
});
$(function() {
App.view = new View({ el: '#test' });
});
If instead of adding an array to the collection, you just pass several objects as arguments (basically just remove the square brackets), the event only fires once.
Is this by design and is there a way to override this behaviour without passing { silent : true } as an option?
The add event is fired once for each model added.
Collection.add can take an array of models, or a single model and some options.
In your example above, you are passing an array of two models in. Since the add event gets fired once for each model added, it fires twice.
When you pass in several objects, Backbone thinks the first object is a model and the second is a hash of options. That means only one model is being added, so it fires the add event once.
Sorry to resurrect this question from the dead, but I was having this problem too and wanted to post how I solved it. The problem with having 'add' trigger so many times for me was because I had a complex render function in my view that was listening for 'add'. This was causing serious performance issues.
I resolved it by creating a temporary collection using backbone's handy collection.clone() method on it, adding the new models to it, and then resetting the original collection with the temp collection's models property. The code looks like this:
// Create a temporary copy of searchResults
var temp = App.searchResults.clone();
// Add the new results
temp.add(newResults.models);
// Copy the new data over the old data
App.searchResults.reset(temp.models);
// Added this since reset triggers 'reset' and my view is listening for 'change add remove'
this.get('filtered_flights').trigger('change');
This sets off only ONE 'change' event instead of several 'add' events.
Related
myView = Backbone.View.extend({
//event binding etc etc
render: function() {
//render some DOM
}
})
anotherView = Backbone.View.extend({
events: {
'click .selector doThis'
},
createNewView: function() {
var view = new myView();
}
})
createNewView may be called multiple times. My understanding is that the variable view will not necessarily be removed by JavaScript's built-in garbage collection because it references objects/code which still exists when the createNewView function completes.
Is this correct? How to deal with this?
My current approach is to initialise myView once at the level of my app:
myApp.view = new myView()
Then in createNewView I just call render on this:
myApp.view.render()
Essentially, I only ever have one of them and I re-use it.
An alternative approach is to track the creation of sub views in an array and then I call .remove() on each one in turn when I know they are no longer needed.
Am I on the right track?
It occurs to me that the second approach is better because if myView created bound callbacks on other objects with listenTo, these would not be removed simply by re-assigning the variable. That is, if I am calling new to instantiate a new instance of the view, I should call remove() on the being discarded instance first... It seems.
In your example, you don't put the view's el into the DOM, so nothing is referencing the view, then it will be collected by the garbage collection.
One good thing to ensure a view isn't bound to something anymore is to call .remove() on it. It will remove:
the view's el from the DOM,
the jQuery DOM events
the Backbone event listeners.
The Backbone .remove source:
// Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners.
remove: function() {
this._removeElement();
this.stopListening();
return this;
},
// Remove this view's element from the document and all event listeners
// attached to it. Exposed for subclasses using an alternative DOM
// manipulation API.
_removeElement: function() {
this.$el.remove();
},
As mentioned by mu is too short in the comments (and myself in almost every other answers), you should always favor listenTo over on or bind to avoid memory leaks and ease unbinding events.
When rendering child views, nested inside a parent view, a good practice is to keep an array of the child views to later call .remove() on each of them.
A simple list view might look like this:
var ListView = Backbone.View.extend({
initialize: function() {
// Make an array available to keep all the child views
this.childViews = [];
},
addOne: function(model) {
var view = new Backbone.View({ model: model });
// as you create new views, keep a reference into the array.
this.childViews.push(view);
this.$el.append(view.render().el);
},
renderList: function() {
// replace the view content completely with the template
this.$el.html(this.templates());
// then cleanup
this.cleanup();
// then render child views
this.collection.each(this.addOne, this);
return this;
},
cleanup: function() {
// quick way to call remove on all views of an array
_.invoke(this.childViews, 'remove');
// empty the array
this.childViews = [];
},
});
Though if other objects are listening to it, it won't be collected and may be a leak. It's important to keep track of the references and delete them all when you don't need it anymore.
I have a backbone page which behaves as follows.
Collection - > Models -> Views
In that I have a collection which contains search results of N length. Each of these models is tied to an instance of a view which in this case is a row of data being displayed.
I want to be able to toggle each row from their 'details' view to their 'advanced' view which contains more information. At the moment I have the parent view rendering N number of views for each model. I can toggled the change in state by updating the model and listening to the change event and then only re-rendering the part of the view I have clicked on. I noticed a problem whilst doing this. The problem is the viewport jump to the top of the page which isn't great UX.
Whilst debugging this I noticed something strange
The parent view's (page that holds the search results) render function is being called, which in turn is calling the render function of each of the rows. I think this is what's causing each of the child views to re-render.
Here are some code examples to demonstrate the problem:
// The child view's render and initialis
var SearchInviteRow = Backbone.View.extend({
tagName: "invite-section-result-row",
initialize: function(params){
this.template = templateHTML;
this.extendedTemplate = extendedTemplate;
this.listenTo(this.model, 'change', this.render);
},
events : {
"click .toggle-attachments" : "renderWithAttachments"
},
render: function(){
var view = this, render;
var that = this;
if(!this.model.get("expand")){
var rendered = Mustache.render(view.template, that.model.toJSON());
this.$el.html(rendered);
} else {
var rendered = Mustache.render(view.extendedTemplate, that.model.toJSON());
this.$el.html(rendered);
}
return this;
},
close: function(){
this.remove();
},
renderWithAttachments: function(){
if( !this.model.get("expand") ) {
this.model.set( {"expand" : true } );
this.model.getAttachments();
} else {
this.model.set( {"expand" : false } );
}
}
});
This is the part of the parent's render that iterates over the collection appending the rows to the search tiles.
for (var i = 0; i < this.collection.length; i++) {
view.subViewArray[i] = new SearchInviteRow({
model: this.collection.at(i)
});
this.$(".invite-section-inside").append(view.subViewArray[i].render().el);
}
What I can't work out is why the parent's render function is being called which is causing my other problems.
There are two ways this can happen, one is if you are listening to the backbone events on your collection the second is if it's a DOM event that's being triggered.
With the backbone events, if you look at the documentation you will see that events triggered on a model in a collection will also be triggered on the collection itself
Any event that is triggered on a model in a collection will also be
triggered on the collection directly, for convenience. This allows you
to listen for changes to specific attributes in any model in a
collection, for example: documents.on("change:selected", ...)
That being the case, you should either check in the events callback whether you want to act on it (see what triggered it, perhaps passing in a extra flag when triggering the event ), or make sure you are only listening to the specific events you are interested in acting on. For example instead of listening to you collections generic change event you might want to listen to the more specific version of it (change:someProperty).
With DOM events, this can easily happen if you are listening to the same selector in your parents view as in your child's view, since the parent's root el is also a parent to the child's el.
I just realized that I have no idea what the heck I am doing when it comes to backbone. I came to this realization when trying to figure out a cogent strategy for removing the view's event listeners on the model. Then I asked "well, where is the model anyways now that the view has been rendered to the DOM?" and then I asked "how is this model object that I created inside a function body, and is therefore out of scope now that I have rendered the view to the DOM, maintaining state?"
AHHHHHHHH!!!!!!!!!
Ex.
View Constructor
Timeclock.Views.JobNewView = Backbone.View.extend({
template: JST['jobs/_form'],
events:{
'blur #job_form :input':'assignValue'
},
initialize: function(options){
this.listenTo(this.model, 'failed-request', this.failedLocationRequest);
this.listenTo(this.model, 'updated-location', this.updatedLocation);
this.listenTo(this.model, 'sync', this.renderJobView);
this.listenTo(this.model, 'invalid', this.displayModelErrors);
this.listenTo($(window), 'hashchange', this.clearListeners);
},
render: function(){
this.$el.html(this.template({attributes: this.model.attributes}));
this.$el.find('#address_fields').listenForAutoFill();
return this;
},
assignValue: function(e){
var $field = $(e.currentTarget)
var attr_name = $field.attr('name');
var value = $field.val();
this.model.set(attr_name, value);
}...
});
Function rendering view to the DOM
renderCollaboratingView: function(e){
var job = this.model;
var $row = $(e.currentTarget);
job.set({customer_id: $row.data('id')});
var model_view = new this.ViewConstructor({model: job});
$container.html(model_view.render().el);
}
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
I understand that backbone views are just a wrapper to declaratively write DOM listeners but how are DOM events acting on the underlying model object in the example above? As soon as the renderCollaboratingView() function has exited how is the model that I passed to the view still being interacted with?
I can think of two ways:
1) The model object is bound to the DOM through a jquery object. All the event listeners that I declare in my view all know where the underlying model object lives on the jquery object(the 'model' attribute?).
2) Backbone is creating some object namespace that the view knows about where it stores models and collections that back the DOM. I have a feeling it's #1 but who knows.
Once again, I got here because I was trying to understand why I need to remove the listeners on the model that I passed into view in the first place. If backbone views are really just jquery objects then aren't jquery listeners removed from DOM elements when the element backing the jquery object is removed from the DOM? Do I only need to remove the listeners if I am going to not destroy the view entirely and save it for later use?
Any help that can be given would be greatly apprecaited. Having an existential crisis.
Thanks
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
Backbone Models and Views are simply Javascript objects that live in-memory in the scope of the page (like any other Javascript). If you were to do ...
var name = 'Peter';
var person = new Backbone.Model({ name: 'Peter' });
var view = new Backbone.View({ model: person } );
... then name, person, and view are all just objects in memory. They have no relation to jQuery; they have no relation to the DOM. The View happens to be able to create DOM elements if you implement render(), but even then those elements don't ever have to ever be attached to the page's live DOM at all.
... how are DOM events acting on the underlying model object in the example above? As soon as the renderCollaboratingView() function has exited how is the model that I passed to the view still being interacted with?
Based on the code you've shown, the model isn't being interacted with directly. Your events hash ...
events:{
'blur #job_form :input':'assignValue'
},
... does say that any time a blur event happens in the job_form element, it will call a method on the view called assignValue. That method may interact with the model (it probably does, right?), but DOM events don't directly cause interaction with the model at all.
If backbone views are really just jquery objects then aren't jquery listeners removed from DOM elements when the element backing the jquery object is removed from the DOM?
Backbone's listeners are wholly different than jQuery listeners. They listen for Backbone-centric events. See here for the list of built-in events that Backbone components fire. A View's events hash is a nice convention that is used to listen for DOM events; it's basically a nice wrapper around the jQuery concept of event delegation.
Do I only need to remove the listeners if I am going to not destroy the view entirely and save it for later use?
If you don't remove listeners, they will continue to run whenever the related event happens, regardless of whether the listening component is changing the page. Suppose you had a Backbone.View that did something like this:
var MyView = Backbone.View.extend({
// ...
events: {
// Don't do this!
'click': '_onClick'
},
// ...
_onClick: function() {
this.$el.append('Clicked!');
}
});
Any time any click DOM event happens on the page, this view will append the text Clicked! to its internal DOM element. When the view is attached to the page's DOM, Clicked! would appear on every click. When the view was removed from the DOM, the function would still run on every click... but since the View's internal root element wasn't attached to anything the function would have no effect.
It's a type of a memory leak, as any instance of MyView will ever be cleared up by the garbage collector. But the particularly nefarious side effect is it also uses CPU time to do something that is completely worthless. Now imagine if the event listener did anything of consequence. Performance of the page will suffer.
JavaScript has garbage collection. Objects do not get destroyed then they go out of scope. An Object X get garbage collected by the runtime system, when it sees that nobody is having a reference (or is pointing) to X.
A Backbone View is also an object. An object can store reference to another object.
In your renderCollaboratingView, you wrote :
var model_view = new this.ViewConstructor({model: job});
this model_view is your view's object. You passed your job which is your model you got from :
renderCollaboratingView: function(e){
var job = this.model;
....
}
You can look at this line in backbone annotated code : BackBone View Options. (I would suggest to look at the link after you have read the answer)
The line is :
var viewOptions = ['model', 'collection', 'el', 'id',
'attributes', 'className', 'tagName', 'events'];
and then Backbone View is defined as : BackBone View
It is :
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
options || (options = {});
_.extend(this, _.pick(options, viewOptions));
this._ensureElement();
this.initialize.apply(this, arguments);
};
Look at line :
_.extend(this, _.pick(options, viewOptions));
and your code :
var model_view = new this.ViewConstructor({model: job});
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
If you merge the dots : You are passing a model to your view. You can also pass other like 'collection', 'el', 'id', ... in viewOptions.
They get pick from your passed object {model: job} and extended in the view object.
This is how your view object has reference to the model that it was given.
Once again, I got here because I was trying to understand why I need to remove the listeners on the model that I passed into view in the first place.
As i said, just removing a view-object from DOM is not going to destroy it. You would have to remove all references of view-object that other objects (here model) have.
When you said :
initialize: function(options){
this.listenTo(this.model, 'failed-request', this.failedLocationRequest);
....
in your view. You told the model to call your view-object's failedLocationRequest on model's event failed-request. This is possible only when your model's object would store reference to view's object. So, your view is not destroyed.
view-object(s) not in dom would continue receiving such events from models and all other places where they registered (except dom) and would do things in the background, that you just never wanted. Definitely not what you wanted..
simple advice, call remove on your view. BackBone View remove
and read stopListening
I'm creating a collection without using fectch(), but with JSON data already available.
this.displays = new Displays(jQuery.parseJSON($('#temp_json').html()));
I need for each model of that collections to have a 'position' value setup, which should be the position of this model in this collection.
What I'm trying to do is to catch an event, where in the initial load each model is constructed from the JSON and added to the collection.
I'll then make something like:
theModel.set('position', this.length);
Unfortunately I can't find that event I should bind my collection to.
Also this collections's models contains other collection (...) where same should be done.
This might seems weird but I have to do this as later on in my view I'll peform things like:
var prototypeNames = [this.model.get('display').get('position'), this.model.get('position')];
Hope this is clear enough.
Thanks!
The Collection.add-function fires a add-event, which you can listen to with the on-function. The event passes the added model AND an options object, which contains some useful attributes (like the index where the model was added), as parameters. (documentation here)
So you'll do something like this:
collection.on('add', function(model, options) {
model.set('position', options.index);
});
if you want to find out more about the parameters the add-event passes, just log them, because the existing documentation is scant at best.
collection.on('add', function(model, options) {
console.log(options);
});
Hope this helps!
Solution that worked for me:
I used backboneRelational, which is awesome when working with actual model relationships.
I'm listening to "relational:add" which is triggered for each item of the collection when constructing it.
I then set some values on my item and trigger a new event 'postAdd' that can be listened to from my view.
window.Displays = Backbone.Collection.extend({
model: Display,
initialize: function(data, options){
this.on("relational:add", function(relModel){
relModel.set('pos',this.indexOf(relModel));
this.trigger('postAdd', relModel);
}, this);
},
});
You can override the default 'add' implementation of the collection to trigger a custom event which won't be silenced by Backbone.
var MyCollection = Backbone.Collection.extend({
// Override the default 'add' implementation...
add: function( models, options ) {
// Call the default implementation first...
Backbone.Collection.prototype.add.apply( this, arguments );
// Fire our custom events on the models...
while (model = models.shift()) {
model.trigger('customAdd', model, this, options);
}
return this;
}
});
You can now listen to the 'customAdd' event triggered on the model. This one will always be triggered, event when silent = true.
As a consequence, when an item is added an with {silent: false} two events will be triggered: 'add' and 'customAdd'.
I've used this technique to fire an event when a new collection has initially loaded and all models are created. Since the initialize method will be called before the models are created, I needed to override the 'reset' function to trigger my code to execute when the models were created.
I have a backbone collection and when I remove a model from the collection, I want it to remove the item from a list in the view.
My collection is pretty basic
MyApp.Collections.CurrentEvents = Backbone.Collection.extend({
model: MyApp.Models.Event
});
and in my views I have
MyApp.Views.CurrentEventItem = Backbone.View.extend({
el: 'div.current_event',
initialize: function(){
event = this.model;
_.bindAll(this, "remove");
MyApp.CurrentEvents.bind('remove',this.remove); //the collection holding events
this.render();
},
// yeah, yeah, render stuff here
remove: function(){
console.log(this);
$(this.el).unbind();
$(this.el).remove();
}
});
when I remove the model from the collection, it triggers the remove function, but the view is still on the page.
In the console, I can see the model, but I believe the model should have an 'el', but it doesn't.
My container code is
MyApp.Views.CurrentEventsHolder = Backbone.View.extend({
el: 'div#currentHolder',
initialize: function(){
MyApp.CurrentEvents = new MyApp.Collections.CurrentEvents();
MyApp.CurrentEvents.bind('new', this.add);
},
add: function(){
var add_event = new MyApp.Views.CurrentEventItem(added_event);
$('div#currentHolder').append(add_event.el);
}
});
for some reason in the add method I can't seem to use the $(this.el) before the append, though I'm not sure if that is the problem.
PROBLEM: MyApp.CurrentEvents.bind('remove',this.remove);
This triggers the remove() function every time any model is deleted from the collection.
This means that anytime a model is deleted, all the CurrentEventItem view instances will be deleted.
Now, about the view still being on the page:
It must have something to do with the way you appended/added/html-ed the view in the page. Check your other views, maybe if you have a CurrentEventsContainer view of some sort, check your code from there because with your current code, it does delete the view, albeit, all of them though.
RECOMMENDED FIX:
change your bindings to:
this.model.bind('remove',this.remove);
and make sure that when you instantiate it, pass on the model so that each view will have a corresponding model to it like so:
//...
addAllItem: function(){
_.each(this.collection, this.addOneItem())
},
addOneItem: function(model){
var currentEventItem = new MyApp.Views.CurrentEventItem({model:model});
//...more code..
}
//...
This makes things a lot easier to manage in my opinion.
UPDATE (from your updated question)
The reason you aren't able to use this.el is because you haven't passed the right context into the add() function. You need to add _.bindAll(this,'add') in your initialize() function to pass the right context, therefore making your this correct, allowing you to use this.el within the add function. Also, change your code to this:
MyApp.CurrentEvents.bind('new', this.add, this); this passes the right context. Also, why not use add instead as an event?
Continuing what I said in the comments, the way you've implemented this right now will remove all the CurrentEventItem views from the page when any of them is removed from the collection. The reason for this is the following:
MyApp.CurrentEvents.bind('remove',this.remove);
What this essentially says is, "every time the remove event is triggered on the collection, call this.remove." So, every time you instantiate one of these views, you're also telling the collection to remove that view when the collection triggers a remove event. I've created a fiddle to show you the problem.
You're right that Backbone knows which model has been removed from a collection, but you're not taking advantage of that. You can do that like so:
removeFromView: function(model) {
// Check to make sure the model removed was this.model
if (model.cid === this.model.cid) {
$(this.el).unbind();
$(this.el).remove();
}
}
See how this minor adjustment changes the behavior? Check it out in action here.
If you follow this pattern, you should see the proper views being removed.