React to events from a collection within a model? - javascript

SubCollection extends Backbone.Collection
Model extends Backbone.Model
subcollection: new SubCollection()
model1 = new Model
model2 = new Model
When the collection in model1 changes I need to update the collection in model2. They cant be a reference to the same collection, when one changes I need to react to the change and apply it to the collection in the other model.
How would I do this? Is this hard to do?
Thanks!

well,
we can't really be sure that there is only the model1 and model2, we could have a model3 and model4, so we cannot actually go binding manually to the models, otherwise you would get a big mess like this:
// not an option... >> huge mess :)
model1.bind('add', myFunction());
model2.bind('add', myFunction());
model3.bind('add', myFunction());
so, what we can do instead
would be to implement an event aggregator in our application. and work with custom events instead.
// application object
var app = {
evt: _.extend({}, Backbone.Events);
};
// subcollection
var SubCollection = Backbone.Collection.extend({
initialize: function(){
_.bindAll(this, "bubbleEvent", "catchBubbledEvent");
this.bind('reset', this.myBubble);
this.bind('add', this.myBubble);
this.bind('reset', this.myBubble);
//... every event you want to catch
app.evt.bind('myCustomEvent', this.catchBubbledEvent);
},
bubbleEvent: function(x, y){
// triggering a general event, passing the parameters
app.evt.trigger('myCustomEvent', x, y, this);
},
catchBubbledEvent: function(x, y, originalCollection) {
// catch any event raised on the event aggregator and cancel out the loop (don't catch events raised by this very own collection :)
if(originalCollection.id === this.id)
return;
// do your stuff here ...
}
});
//model
var myModel = Backbone.Model.extend({
// notice me setting a unique ID in the collection, i pass in the client id of this instance of the model
subCollection: new SubCollection({id: this.cid});
});
so basically we catch every event of the collection we want to, then we pass it trough to a general event on the single event Aggregator we have for our whole app, anything can bind to that, and do stuff when the proper event is raised, so can our collection bind to it, and do stuff. since this is possible that your collection catches the event that it sent out himself, we need a small test to cancel out these situations... and only continue when another collection raised this event.

Related

Best method for rendering collection in Backbone

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.

Backbone.js: Model events are not fired while used within a Collection

Here I have a Backbone.js Model - Contact and a Collection - Contacts with Contact as the Model. I've two views where in one I use collection to list all the contacts and in the second the model directly to show a single contact. But when I'm using the model directly I'm able to get the 'change' event fired while using with the collection the 'change' event, (even 'all' events of the model) is not fired. Am I missing some extra bind here to make it work with collection?
var Contact = Backbone.Model.extend({
defaults: {
id: 0,
urlDisplayPicBig: '',
urlDisplayPicSmall: ''
},
initialize: function () {
this.bind('change', this.doSomething);
},
generateUrls: function () { //earlier doSomething()
...
}
...
});
var Contacts = Backbone.Collection.extend({
model: Contact,
...
});
Update
While using both collection & single model instances I have to run generateUrls() to update urlDisplayPicBig & urlDisplayPicSmall based on the 'id' of the model.
When you do fetch on a collection:
the collection will reset
and that will
replace a collection with a new list of models (or attribute hashes), triggering a single "reset" event at the end. [...] Using reset with no arguments is useful as a way to empty the collection.
So a fetch on a collection will remove all the models that are currently in the collection and replace them with brand new model instances. No "change" events will occur, there will only be a single "reset" event.
When you do a fetch on a model:
A "change" event will be triggered if the server's state differs from the current attributes.
So calling fetch is pretty much the same loading the data from the server manually and calling set to change the model.
Summary: Collection#fetch doesn't trigger any "change" events, just a "reset" event; Model#fetch will trigger a "change" event if something changes.
If you just want to add a couple new attributes when creating new model instances then you can add new attributes to the incoming JSON using Model#parse.

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.

What can my models bind to when a collection is reset?

Are there any events my models can bind to, to know their collection has been reset?
When I call:
collection.reset()
I want those removed models to be destroyed and in turn any views to know they are gone. What should I bind to here?
From the fine manual:
reset collection.reset(models, [options])
[...] triggering a single "reset" event at the end.
So bind to the collection's reset event and hope that no one uses the {silent: true} option to do things behind your back.
#mu's answer is correct, but you might also need to know that a model that is added to a collection has the .collection property, which points to the parent collection. So if you are instantiating your models manually, you can just do this:
var myModel = new MyModel();
collection.add(myModel);
collection.bind('reset', model.cleanUp(), model);
But if you're instantiating your models via the collection, e.g. with collection.fetch(), you need to bind to the collection in the initialize() method of the model:
var MyModel = Backbone.Model.extend({
initialize: function() {
if (this.collection) {
this.collection.bind('reset', this.cleanUp(), this);
}
}
// etc
});

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

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.

Categories