Backbone.js binding a change event to a collection inside a model - javascript

I'm pretty new to Backbone so excuse me if this question is a little obvious.
I am having problems with a collection inside of a model. When the collection changes it doesn't register as a change in the model (and doesn't save).
I have set up my model like so:
var Article = Backbone.Model.extend({
defaults: {
"emsID" : $('html').attr('id')
},
initialize: function() {
this.tags = new App.Collections.Tags();
},
url: '/editorial_dev.php/api/1/article/persist.json'
});
This works fine if I update the tags collection and manually save the model:
this.model.tags.add({p : "a"});
this.model.save();
But if the model is not saved the view doesn't notice the change. Can anyone see what I am doing wrong?

initialize: function() {
this.tags = new App.Collections.Tags();
var model = this;
this.tags.bind("change", function() {
model.save();
});
},
Bind to the change event on the inner collection and just manually call .save on your outer model.

This is actually an addendum to #Raynos answer, but it's long enough that I need answer-formatting instead of comment-formatting.
Clearly OP wants to bind to change and add here, but other people may want to bind to destroy as well. There may be other events (I'm not 100% familiar with all of them yet), so binding to all would cover all your bases.
remove also works instead of destroy. Note that both remove and destroy fire when a model is deleted--first destroy, then remove. This propagates up to the collection and reverses order: remove first, then destroy. E.g.
model event : destroy
model event : remove
collection event : destroy
collection event : remove
There's a gotcha with custom event handlers described in this blog post.
Summary: Model-level events should propagate up to their collection, but can be prevented if something handles the event first and calls event.stopPropagation. If the event handler returns false, this is a jQuery shortcut for event.stopPropagation();
event.preventDefault();
Calling this.bind in a model or collection refers to Underscore.js's bind, NOT jQuery/Zepto's. What's the difference? The biggest one I noticed is that you cannot specify multiple events in a single string with space-separation. E.g.
this.bind('event1 event2 ...', ...)
This code looks for the event called event1 event2 .... In jQuery, this would bind the callback to event1, event2, ... If you want to bind a function to multiple events, bind it to all or call bind once for each event. There is an issue filed on github for this, so this one will hopefully change. For now (11/17/2011), be wary of this.

Related

How to delete old backbone views

I have searched the internet for ways to trigger the destruction of old views.
There are functions to do this, however, I don't know how to trigger them. Ideally, there would be a way to trigger the destruction on the event of closing a view.
I can't find a way how to trigger that particular event.
You should call view.remove() to trigger its destruction as specified in the documentation http://backbonejs.org/#View-remove
For example, if you had:
var myView = Backbone.View.extend({
initialize: function() {
...
},
render: function() {
...
}
});
You can later call myView.remove() provided you have a reference to myView available.
This method should also remove any event listeners tied to the view if you are using the listenTo (recommended) method as opposed to the on listener. You could also add view.off() to ensure that the events are removed.
Additionally, you will need to add a way for views to listen to a close event so you can call the remove and off methods. You should refer to 1 and 2.
This old but fantastic piece by Derick Bailey does a great job at explaining the issue and how to solve it. As Monica rightly suggested this relies on view.remove() but you can update your router to destroy your existing view -
Try something similar to
if (currentView) {
currentView.remove();
currentView = newView();
}

model events are not working in Marionette

I am using Marionette.js for making an Application.
model event was not triggering If I change Attribute value in view.initialize function just like in the following way.
var ChartsView = Marionette.CompositeView.extend({
initialize:function(){
this.model.set({"currentJson":""},{silent:true});
this.model.set({"currentJson":"test"});
},
modelEvents:{
"change:currentJson":"settingOtherAttributesInSameModel"
},
settingOtherAttributesInSameModel:function(){
console.log("This is Testing");
}
});
while instance creation time, I am passing the model object just like in the following way.
chartsViewObj=new ChartsView({el:$('#plannerTablePlace'),model:chartsModelObj});
I don't know, why model event was not working.
I didn't find,where I wrong.
can anyone help me.
Your problem is that the delegateEvents function that bind the view events is called after the initialize function. Thus when you do this :
initialize:function(){
this.model.set({"currentJson":""},{silent:true});
this.model.set({"currentJson":"test"});
}
The attribute is set and the change event is triggered but the view events are not yet bound.
So if you want the events to be triggered, do like this :
chartsViewObj=new ChartsView({el:$('#plannerTablePlace'),model:chartsModelObj});
chartsModelObj.set({"currentJson":""},{silent:true}); // this will not trigger the event because of 'silent:true'
chartsModelObj.set({"currentJson":"test"});

Backbone collection.fetch({update: true}) doesn't compare id add's same model again

In relation with My another question on Backbone collection View add not being called with a model
I've my models
var Client = Backbone.Model.extend({
idAttribute: 'ip'
});
var Colony = Backbone.Collection.extend({
url: '/presence/knock',
model: Client
});
I've also tried
var Client = Backbone.Model.extend({
ipAttribute: function(){
return this.options.ip
}
});
I've also tried using the id attribute instead of using ip. But nothing works. It seems Backbone is comparing the whole object. not the idAttribute So If two objects are EXACTLY same its not triggering add However If there is any change which is not in idAttribute is still thinks the model is new and triggers add
I've a Fiddle on http://jsfiddle.net/3QE3u/ that uses mock ajax (based on the solution of the above mentioned question that I asked few days ago).
If there is any change which is not in idAttribute is still thinks
the model is new and triggers add
In your code in the fiddle you have also binded "change" event of the collection to "addAll" function
this.collection.bind('change', this.addAll);
If you comment this line, your code will work as you expected.
Here it how it works now:
When first fetch function is called "add" event is fired for the item with ip "127.0.0.2" and "change" is fired for the item with ip "127.0.0.1". When fetch function is called for the second time "change" event is fired for both items. For each consecutive call to fetch, "change" event is fired for two items. (side note: refresh event is never trigger at all)
You can write another method to handle the "change" event and in that method you can update existing html element to reflect the update.

Backbone.js: how to unbind from events, on model remove

in backbone we have an app that uses an event Aggregator, located on the window.App.Events
now, in many views, we bind to that aggregator, and i manually wrote a destroy function on a view, which handles unbinding from that event aggregator and then removing the view. (instead of directly removing the view).
now, there were certain models where we needed this functionality as well, but i can't figure out how to tackle it.
certain models need to bind to certain events, but maybe i'm mistaken but if we delete a model from a collection it stays in memory due to these bindings to the event aggregator which are still in place.
there isn't really a remove function on a model, like a view has.
so how would i tacke this?
EDIT
on request, some code example.
App = {
Events: _.extend({}, Backbone.Events)
};
var User = Backbone.Model.extend({
initialize: function(){
_.bindAll(this, 'hide');
App.Events.bind('burglar-enters-the-building', this.hide);
},
hide: function(burglarName){
this.set({'isHidden': true});
console.warn("%s is hiding... because %s entered the house", this.get('name'), burglarName);
}
});
var Users = Backbone.Collection.extend({
model: User
});
var House = Backbone.Model.extend({
initialize: function(){
this.set({'inhabitants': new Users()});
},
evacuate: function(){
this.get('inhabitants').reset();
}
});
$(function(){
var myHouse = new House({});
myHouse.get('inhabitants').reset([{id: 1, name: 'John'}, {id: 1, name: 'Jane'}]);
console.log('currently living in the house: ', myHouse.get('inhabitants').toJSON());
App.Events.trigger('burglar-enters-the-building', 'burglar1');
myHouse.evacuate();
console.log('currently living in the house: ', myHouse.get('inhabitants').toJSON());
App.Events.trigger('burglar-enters-the-building', 'burglar2');
});​
view this code in action on jsFiddle (output in the console): http://jsfiddle.net/saelfaer/szvFY/1/
as you can see, i don't bind to the events on the model, but to an event aggregator.
unbinding events from the model itself, is not necessary because if it's removed nobody will ever trigger an event on it again. but the eventAggregator is always in place, for the ease of passing events through the entire app.
the code example shows, that even when they are removed from the collection, they don't live in the house anymore, but still execute the hide command when a burglar enters the house.
I see that even when the binding event direction is this way Object1 -> listening -> Object2 it has to be removed in order to Object1 lost any alive reference.
And seeing that listening to the Model remove event is not a solution due it is not called in a Collection.reset() call then we have two solutions:
1. Overwrite normal Collection cleanUp
As #dira sais here you can overwrite Collection._removeReference to make a more proper cleaning of the method.
I don't like this solutions for two reasons:
I don't like to overwrite a method that has to call super after it.
I don't like to overwrite private methods
2. Over-wrapping your Collection.reset() calls
Wich is the opposite: instead of adding deeper functionality, add upper functionality.
Then instead of calling Collection.reset() directly you can call an implementation that cleanUp the models before been silently removed:
cleanUp: function( data ){
this.each( function( model ) { model.unlink(); } );
this.reset( data );
}
A sorter version of your code can looks like this:
AppEvents = {};
_.extend(AppEvents, Backbone.Events)
var User = Backbone.Model.extend({
initialize: function(){
AppEvents.on('my_event', this.listen, this);
},
listen: function(){
console.log("%s still listening...", this.get('name'));
},
unlink: function(){
AppEvents.off( null, null, this );
}
});
var Users = Backbone.Collection.extend({
model: User,
cleanUp: function( data ){
this.each( function( model ) { model.unlink(); } );
this.reset( data );
}
});
// testing
var users = new Users([{name: 'John'}]);
console.log('users.size: ', users.size()); // 1
AppEvents.trigger('my_event'); // John still listening...
users.cleanUp();
console.log('users.size: ', users.size()); // 0
AppEvents.trigger('my_event'); // (nothing)
Check the jsFiddle.
Update: Verification that the Model is removed after remove the binding-event link
First thing we verify that Object1 listening to an event in Object2 creates a link in the direction Obect2 -> Object1:
In the above image we see as the Model (#314019) is not only retained by the users collection but also for the AppEvents object which is observing. Looks like the event linking for a programmer perspective is Object that listen -> to -> Object that is listened but in fact is completely the opposite: Object that is listened -> to -> Object that is listening.
Now if we use the Collection.reset() to empty the Collection we see as the users link has been removed but the AppEvents link remains:
The users link has disappear and also the link OurModel.collection what I think is part of the Collection._removeReference() job.
When we use our Collection.cleanUp() method the object disappear from the memory, I can't make the Chrome.profile tool to explicitly telling me the object #314019 has been removed but I can see that it is not anymore among the memory objects.
I think the clean references process is a tricky part of Backbone.
When you remove a Model from a Collection the Collection takes care to unbind any event on the Model that the Collection its self is binding. Check this private Collection method.
Maybe you can use such a same technique in your Aggregator:
// ... Aggregator code
the_model.on( "remove", this.unlinkModel, this );
// ... more Aggregator code
unlinkModel: function( model ){
model.off( null, null, this );
}
This is in the case the direction of the binding is Aggregator -> Model. If the direction is the opposite I don't think you have to make any cleaning after Model removed.
Instead of wrapping Collection's reset with cleanUp as fguillen suggested, I prefer extending Collection and overriding reset directly. The reason is that
cleanUp takes effect only in client's code, but not in library(i.e. Backbone)'s.
For example, Collection.fetch may internally call Collection.reset. Unless modifying the Backbone's source code, we cannot unbind models from events(as in cleanUp) after calling Collection.fetch.
Basically, my suggested snippet is as follows:
var MyCollection = Backbone.Collection.extend({
reset: function(models, options) {
this.each(function(model) {
model.unlink(); // same as fguillen's code
});
Backbone.Collection.prototype.reset.apply(this, arguments);
}
});
Later, we can create new collections based on MyCollection.

backbone.js passing extra data along with events

Using backbone.js, when a collection's remove method is called a "remove" event is fired.
How can I extend this "remove" event to pass extra data, such as certain attributes of the particular model being removed?
How can I bind to the "remove" event triggered by a specific model, specified by id or cid?
I suppose any solution would also be applicable for the "change" event? Thanks for help.
If you're removing a model from a collection, you shouldn't need that model anymore. I guess I'm missing the point of extending remove to do more than just remove something.
When you call remove on a collection, you pass the model or an array of models in the collection to the remove function. I would recommend doing any last minute work you need with those models just before you call the remove function on your collection. At that point you should have all the models and their attributes that you plan on removing.
To bind to the change event of a specific model you just need to get the model you want from the collection and bind to that:
var myModel = myCollection.get(id); //using the id of the model
or
var myModel = myCollection.getByCid(cid); //using the cid of the model
Now bind to that model:
myModel.bind("change", function() {
//do something
});
or, bind change to all the models in a collection:
myCollection.bind("change", function(model) {
//do something, model is the model that triggered the change event
});

Categories