Turn Backbone events off when Collection is disposed of - javascript

I have my Collection subscribing to an event I trigger on the Backbone object itself:
const Items = BaseCollection.extend({
model: ItemModel,
initialize() {
BaseCollection.prototype.initialize.apply(this, arguments);
Backbone.on('api:data:foobar', (data) = > {
});
}
});
However, when I create new instances of this Collection, Backbone adds another listener. When this event is triggered, the callback is fired many many times. Is there a way to either:
Only set this once per Collection lifecycle?
Where is the best place to unsubscribe/turn off this event listener?

The reason this is happening is because your binding the event handler directly to the Backbone object, every time you create a new instance (which calls the initialize method of your collection).
What you want to do instead is invert the relationship and have your collection listen to the event and unsubscribe when you are disposing of your collection. Since version 0.9.9 Backbone provides a built it way to do this using the listenTo.
For example
initialize() {
BaseCollection.prototype.initialize.apply(this, arguments);
this.listenTo(Backbone, 'api:data:foobar', (data) = > {
});
To unsubscribe/turn off this event listener you can use the the stopListening method (they should also be automatically removed if you remove your collection). With views when you call remove it will automatically call stopListening.
If you really want to use bind the event handler directly to the Backbone object and bind it just once you could do this in this by moving the event binding outside of your initialize method, but you will probably be better off using listenTo.

Related

Backbone model objects.... where are they stored so that the DOM can interact with them?

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

angular.js $destroy event - should I manually unbind?

I'm trying to figure out if angular base automatically unbinds watchers and scope events bound with $scope.$on(...) or $scope.$watch(...) when scope is destroyed?
Suppose I have following code:
$scope.$on('someEvents', handleSomeEvent);
$scope.$watch('someProperty', handleSomePropertyChange);
Do I need to manually unbind these watchers and events when $destroy event is triggered on scope?
According to Angular documentation on $scope:
'$destroy()' must be called on a scope when it is desired for the scope and its child scopes to be permanently detached from the parent and thus stop participating in model change detection and listener notification by invoking.
Also
Removal also implies that the current scope is eligible for garbage collection.
So it seems when $destroy() is called all the watchers and listeners get removed and the object which represented the scope becomes eligible for garbage collection.
If we look at the destroy() source code we'll see a line :
forEach(this.$$listenerCount, bind(null, decrementListenerCount, this));
Which is supposed to remove all the listeners.
As mentioned by #glepretre it applies to the watchers and listeners in the controller. The same doc page listed above says that:
Note that, in AngularJS, there is also a $destroy jQuery event, which can be used to clean up DOM bindings before an element is removed from the DOM.
So if you have specific listeners in the directives you should listen to the $destroy event and do the necessary cleanup yourself
If the scope is in a controller, angular unbind for you. Else you can unbind your event by calling the returned function :
var myevent = $scope.$on('someEvents', handleSomeEvent);
myevent() ; // unbind the event
http://docs.angularjs.org/api/ng/function/angular.bind
As previously answered, Angular indeed takes care of cleaning things for you, whenever possible. So if you do $scope.$on('someEvents', handleSomeEvent);, once the scope is destroyed (eg when you go to another page/view in your app), the event is automatically removed.
One important thing to note though, is that $rootScope is of course never destroyed, unless you quit your app. So if you do $rootScope.$on('someEvents', handleSomeEvent);, you may have to remove the event yourself, depending on where you listen to the event:
if in a controller or directive, then you'll have to remove it manually, else each time you'll instantiate the controller, a new event will be attached, and so handleSomeEvent will be called many times
if in a service, then you do not need to remove it manually, as services are always singleton (note that in Angular service, factory, ... all end up being the same thing)

Backbone collection add method being fired twice

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.

BackboneJS catch event OnAdd from initial load

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.

Pass data into change event callback?

Using backbone.js - I want to bind an event to any change in the model and the collections/models that are nested inside it.
Right now I do an #bind 'change', () -> when initializing the base model.
How can I pass data on any change, even that of deep nested models? Does the change event carry variables with it? I need things like the model's collection, id, attributes, etc.
Thanks.
Does the change event carry variables with it?
Yes, the change event gets two arguments: First, the model itself; second, the new attribute value. There are several methods available on the model that are aimed specifically at getting information during a change event. See the docs on hasChanged, changedAttributes, previous, and previousAttributes.
So for instance, to access the previous attributes of a model each time it changes, you'd write
#bind 'change', (model) ->
prevAttrs = model.previousAttributes()
...

Categories

Resources