I have a Backbone View that has a Collection as its Model. If the collection is passed in through its constructor it can add listeners to the collection in its initialize function, but how can it know when its collection is set after construction so that it can listen to events from the collection?
I want to be able to change its collection during its lifecycle and have it re-render based on the data in the new collection, but there seems to be no way of it knowing when its collection has changed? Are there any hooks available?
[Note: See my answer below with code based on stusmith's answer]
I don't think there is any automatic way to know - collection is just an ordinary property.
You could always provide a function setCollection instead, which unbinds events from the old collection (if any), assigns the collection, and rebinds to the new one.
For clarity, you would also call this function from initialize.
Here is my solution based on stusmith's answer:
initialize: function(){
if(this.collection){
this.addCollectionListeners();
}
},
setCollection:function(collection){
if(collection != this.collection){
if(this.collection){
this.removeCollectionListeners();
}
this.collection = collection;
this.addCollectionListeners();
}
},
removeCollectionListeners:function(){
//Remove listeners
},
addCollectionListeners:function(){
//Add listeners
},
Related
Newish to Backbone, and having some trouble. Going to try to ask this in a generic, no-code way since the application I'm tasked with maintaining is several thousand lines long... hope I can be clear.
I have a method myMethod(), that belongs to a model App.Person.
I have a collection App.PersonList that holds several instances of App.Person.
I have an instance (myPersonList) App.PersonList that I'm creating within an object (myDonationForm) that is an instance of an object App.DonationForm (and here we roam even further outside my comfort zone: App.DonationForm extends an object named Controller which extends an object called Base which seems to be a base.js thing and I have very little idea what's happening here but I hope it doesn't matter for my immediate need).
Also in App.DonationForm, I have an instance (myErrorMsg) of a model App.Errors. I would like to be able to set an attribute of myErrors from myMethod() but can't work out the syntax to refer to myErrors, traversing up the tree of nested objects and then back down a parallel step.
I hope that made sense. To visualize it:
myDonationForm, inst of App.DonationForm, ext Controller
|--myPersonList, inst of App.PersonList, ext Collection
| |--myPerson[1], inst of App.Person, ext Model // I want to change from here
| | +---myMethod()
| |--myPerson[2], inst of App.Person, ext Model // or from here
| +---myMethod()
+myErrorMsg, inst of App.Errors, ext Model // an attribute of this.
Thank you in advance for any pointers you can offer.
Edited to add a code snippet (and I accidentally tried to edit Hoyen's answer, not my own question! didn't realize it til I got the peer review screen, ugh)
App.SpecialDonationForm = App.DonationForm.extend({
[...]
initialize: function(options){
App.DonationForm.prototype.initialize.call(this, options);
[...]
},
start: function(){
App.DonationForm.prototype.start.call(this);
[...]
this.myPersonList = new App.PersonList(this.initialData);
this.myErrorMsg = new App.Errors();
[...]
Basically since myErrorMsg is an instance of a Backbone model you need to use Backbone's model methods to set the attributes. So, it should look something like this:
App.Person = Backbone.Model.extend(
{
[...]
defaults: {
[...],
errorMsg: new App.Errors()
},
[...]
myMethod: function(){
var value = ""; // set it to what ever you like
this.get("errorMsg").set("message",value); // "message", is the attribute you want to update with the value
this.trigger('change:errorMsg'); // not sure if this is needed. but this will insure that this will trigger any event listeners on errorMsg
}
});
App.SpecialDonationForm = App.DonationForm.extend({
[...]
initialize: function(options){
App.DonationForm.prototype.initialize.call(this, options);
[...]
},
start: function(){
App.DonationForm.prototype.start.call(this);
[...]
this.myPersonList = new App.PersonList(this.initialData);
this.myErrorMsg = new App.Errors();
this.myPersonList.on('change:errorMsg',showError.bind(this));
function showError(model,val,options){
this.myErrorMsg.set(_.extend(this.myErrorMsg.defaults,model.toJSON());
}
[...]
An event aggregator may work in this scenario.
Setup to extend Backbone events
App.eventAgg = _.extend({object}, Backbone.Events);
During App.Errors initialize, listenTo an event
this.listenTo(App.eventAgg, 'someEvent', this.doSomeUpdate);
In App.Person's myMethod, trigger the event
App.eventAgg.trigger('someEvent');
To answer your generic question even more generically,
Based on your hierarchy, a Person object doesn't know anything at all about the Errors object. Child objects don't (and shouldn't) know anything about their parents.
A Person object does (presumably) know, however, that an "error" has occurred and someone might want to know about it.
Since the Person object doesn't know any more than that, all it can do is trigger a custom event.
Some other object needs to listen for this custom event. Based on your hierarchy, the only other object that has access to the Person object is the PersonList collection.
The PersonList collection is now in the same situation. It has learned (from the custom event) that an error has occurred, but it can't do anything about it and it doesn't know about its parents. So all it can do is trigger another custom event.
The DonationForm object has to listen for the custom events from the PersonList collection. When it receives the event, it can then pass it to the Errors object.
Voila.
So there's a piece of functionality with which I've been struggling for a while now. I'm using .where() method in order to retrieve an array of objects from the Collection and then I reset this Collection with this array.
# Fetch the collection
collection = App.request(collection:entites)
console.log collection
>collection {length: 25, models: Array[25] ... }
When the event fires it passes options for .where() method and starts reset process:
# Get new models
new_models = collection.where(options)
# Reset collection with the new models
collection.on 'reset', (model, options) ->
console.log options.previousModels
return
collection.reset(new_models)
console.log collection
>collection {length: 5, models: Array[5] ... }
In the View responsible of rendering this collection I listen to the 'reset' event and render the View accordingly.
initialize: ->
#listenTo(#collection, 'reset', #render)
It works just as expected: event fires, collections undergoes reset and the View re-renders reseted collection. But when the event fires second time the collection doesn't sync with the server and new_models = collection.where(options) receives a collection that was already reseted in a previous event run and returns an empty array.
What are my options here? Each event run I need an initial collection of all models to work with. Should I just request new instance of the collection on the each run or can I make it in a more cleaner manner, i.e. save original state somewhere and pass it for the event run instead of fetching new collection from the server? Please advise.
Yes. Another way to achieve it is, when you filter the collection, using .where(), you can trigger Backbone.Events custom event which you view can listen to. This way, the original collection does not reset, there only is a change in array which will trigger the custom event.
A sample code for using custom events in backbone is:
var object = {};
_.extend(object, Backbone.Events);
object.on("collectionFiltered", function(arrayOfFilteredModels) {
// do stuff to render your view with the new set of array.
// You can use underscore templating to traverse the array for rendering view.
});
object.trigger("collectionFiltered", collection.where(options);
I have a ListView and InstanceView defined in my Backbone code. The ListView is associated with a collection and it instantiates an InstanceView like so
render: function () {
this.collection.forEach(function(instance){
var commentHTML = new InstanceView({
model: instance
}).render();
renderedComments.push(commentHTML);
});
}
The new view instance goes out of scope after the render call finishes. What I've noticed is that the view persists in memory, though. I can tell because the events attached to it still fire long after the render method terminates.
So, does the view avoid gc because of its reference to the model object which, in turn, is referenced by the collection?
When the view is created events are registered on the model. The callbacks have references to the view, preventing it from being gc'ed.
Check out the backbone code on github. The events are hooked up in the delegateEventsmethod.
Objects are garbage collected whenever the JS engine feels like collecting them.
Your question, however, is actually different.
Just because you cannot access an object does not mean:
Nothing can, and
Things it attaches also go away.
Also, you explicitly add the object to collection, so it's not eligible for collection.
I want to remove a model from a collection:
var item = new Backbone.Model({
id: "01",
someValue: "blabla",
someOtherValue: "boa"
});
var list = new Backbone.Collection([item]);
list.get("01").destroy();
Result:
item is still in the backbone array ....
I have reservations about the accepted answer. When a model is destroyed, the "destroy" event bubbles through any collection the model is in. Thus, when you destroy a model you should not have to also remove the model from the list.
model.destroy();
Should be enough.
Looking at your code it looks correct: (If the intent is to destroy + remove, not just remove)
list.get('01').destroy();
Are you sure that your resource is getting properly destroyed? Have you tried placing a success and error callback in your destroy() call to ensure the destroy was executed? For example, if your model URL is incorrect and it can't access the resource, the destroy call would return an error and your collection will still have the model. This would exhibit the symptoms you outline in your question.
By placing the remove() after the destroy call, your collection will definitely remove the model. But that model will still be floating around (still persisted). This may be what you want, but since you're calling destroy() I'm assuming you want to obliterate it completely. If this is the case, while remove seems to work, what it's really doing is masking that your model has been destroyed when in fact it may not.
Thus, this is what I have a feeling is actually happening. Something is preventing the model from being destroyed. That's why when you call destroy(), then check your collection - the model is still there.
I could be wrong though. Could you check this and update your findings?
You should also remove the model from the collection.
var model = list.get('01');
model.destroy();
list.remove(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.