How does collection.fetch({add:true}) work? - backbone.js

I added a model to my collection and did model.save The data gets saved on the server.
After this I call collection.fetch({add:true}). Ideally only one model should be returned from server ie the newly added one but I'm seeing the entire collection being propagated back in chrome developers tool.
I'm confused. how does this actually work ?

As an add-on to Cyril N.'s answer (which describes essentially the default behavior, and should be enough I guess), I'd like to explain more thoroughly the role of the 3 flags:
add: whether or not Backbone should create and add to the collection the models which don't exist yet (ie, whose id are not in the collection)
remove: whether or not Backbone should remove from the collection the models which were not brought back from the server (ie, whose id were not is the brought back data)
merge: whether or not Backbone should update the models which are not in the first two categories (already in the collection, and brought back from the server)
However, I like to expand a bit further about the general behavior of the method (it uses the Collection#set method as a callback, so it's more the Collection#set's behavior). Backbone prepares the models, which means it creates fake, volatile models at the beginning of the method (they're only volatile if they're not added). This can lead to unexpected behaviors: because it creates models, the initialize method of those are executed.
Also, as a side-note, since Backbone 1.0, the Collection#reset flag was introduced if you want to reset your collection when fetching (therefore it uses this method as a callback). Before that, it was the default behavior of the Collection#fetch method.
Well, with both answers you should have every info you need I guess.

When you call collection.fetch({add:true}, Backbone will request the server with the complete list of items in the collection (hence the entire collection you see in the Chrome developers tool), but then, instead of reloading the entire collection in the Collection Object in Backbone (Javascript), Backbone will performs a "smart" update of the collection.
The set method performs a "smart" update of the collection with the
passed list of models. If a model in the list isn't yet in the
collection it will be added; if the model is already in the collection
its attributes will be merged; and if the collection contains any
models that aren't present in the list, they'll be removed. All of the
appropriate "add", "remove", and "change" events are fired as this
happens. If you'd like to customize the behavior, you can disable it
with options: {add: false}, {remove: false}, or {merge: false}.
Source: http://backbonejs.org/#Collection-set
All the smart work is made client side, in Javascript, but for that, Backbone request the entire collection from the server to compare it with what it has in local (client side).
(I hope I'm clear ;)).

Related

Pagination with collection.fetch() in backbone.marionette

I am trying to implement pagination with backbone.marionette's
compositeView by first doing an initial fetch:
collection.fetch({reset: true})
To load additional models into the same collection,
collection.fetch({reset: false, remove: false})
is used.
I saw that on each use of fetch() with {reset: false}
muliple add events are triggered (for every model added.)
Thats fine basically, but when looking into the annotated source
of the compositeView's appendHtml function in marionette the follwing
statement is given:
buffering happens on reset events and initial renders in order to
reduce the number of inserts into the document, which are expensive.
While this is great for the initial fetch (only 1 browser reflow), every
subsequent fetch with {reset: false} could get pretty expensive
(reflow on every single model thats added). It really seems to
append() the itemView every single time - causing a reflow
(compositeView.isBuffering is false).
Is there any way to "batch" process the models and do a single insert
(same as done on the reset event)?
Does this need an overwrite in the appendHtml implementation
and implement the batch logic by myself?
(I assume you don't want to use Backbone.paginator, for some reason. If that isn't the case, use the plugin ;-D).
You could use a technique similar to the filtered collection used here: https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/entities/common.js#L2 (instanciation at https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/apps/contacts/list/list_controller.js#L13).
Basically: you provide a "paginated collection" to the view, and what the paginated collection does is:
initially, return a "cloned" copy of the original collection
when you fetch on the original collection, add the fetched events in "batch" to the cloned collection, which will trigger the view to refresh (because it's listening to the cloned collection, not the original)
To minimize reflows, you can try the following:
silence the events that the collection listens to ("add", etc.)
when a group of models gets added, create a new instance of a collection view with only the new models
render the new collection view (but don't show it anywhere), and manually append the newView.el to the right place in the DOM
destroy the newView
Unfortunately, once you need to start optimizing, you tend to need to handle a bunch of things by hand...

Backbone keeps creating new models

Everytime I do a "fetch" of my collection, backbone creates new models for every item. The old models stick around in memory, causing a big memory leak.
There are no changes of the data between "fetch" calls, should backbone not recognize that there are no changes and carry on?
Backbone collection will use "set" method to update model data in collection.
Set is a smart method and it will perform these checks when collection fetch data from server:
Add: If a model in the list isn't yet in the collection it will be added.
merge: if the model is already in the collection its attributes will be merged
remove: if the collection contains any models that aren't present in the list, they'll be removed
Your problem is the collection will continuously add new model when you fetch data from server even though you just try to refresh data which is updated. I think your data don't have unique "id" attribute. So the collection can't perform "smart update" when it fetch data from server.
{id:"1234",name:"blabla","tel:0600000000"}
I hope this is helpful for you.
The problem is different than I thought.
I keep a list of sub-view in my view so I can remove them. However, there appear to be 2 different properties with the same name. One keeping a hold of the views.

Passing data from main application view to some subviews

Please consider following Backbone application structure:
AppView
Subview
FirstSubviewInQuestion
Subview
Subview
SecondSubviewInQuestion
Application view creates and stores a collection of special items. At a certain time the first and the second subview needs to get access to that collection. Now what is the best way to pass the collection it to them?
I myself see some ways, yet each has a downside:
1) Create instance of that collection not inside the App View but even more globally and then pass it around as dependency (project uses RequireJS; so it would be passed to AppView and both Subviews).
Downsides: app view is supposed to be the top most element in application. Yet suddenly we have some instances of collections floating above.
2) Or I can do following:
// some subview
var collection = {};
Backbone.trigger( "item:getSpecialItems", collection);
// now the collection has data
// app view
this.listenTo( "item:getSpecialItems", function(obj) {
// copy collection to passed object
_.extend(obj, this.specialCollection);
});
Downsides: we are triggering a global event. We know that only the app view would respond, but it feels like a bad design. Plus, this way to pass the collection seems like a hack.
Maybe there are some other clever ways to do it?
I would say #1.
app view is supposed to be the top most element in application
Right, but you're talking about (I think) a Collection, not a View; the two are totally separate parts of your application (in MVC the first is the "M" and the second is the "V"). By definition, if your views are driven by your data, the data must be "higher" (in the dependency tree) than any view, so I don't see anything wrong with that approach.
If your goal is to get a shared instance of the collection, my approach would be to pass that down from the parent to "subview" and from subview into its children. However, I wouldn't necessarily use require to pass around a singleton of this collection.
You could also pull out all of the logic from the view regarding the special methods for "creation and storage of special objects" into a helper class whose sole domain is just this. That helper then becomes a utility available from outside the view hierarchy, perhaps even globally or via require.

backbone.js: does calling collection.reset() removes the models as well?

I read in the backbone documentation that calling collection.reset() clears the collection. I want to know if it removes the models as well or do they continue to live in memory?
If they're not removed is there an easier way to remove all the models in a collection without iterating through the models and calling model.remove()?
You could listen for the reset event from the model and do your cleanup and this.destroy() in response. That's what the event hooks are for. See http://backbonejs.org/#Events-catalog
Note: You absolutely should not change or override any method or property prefixed by an underscore, such as _removeReference. The underscores mean that it is intended as an internal method or property, and that the internal implementations may change (their API's are considered unstable). Upgrading Backbone could break any code that relies on underscore-prefixed methods, even if the release is advertised as a backwards-compatible change.
I know your question says "without iterating", but it really is the most reliable way of handling this. Consider a case where a model has been moved from one collection to another, but it's still listening on the first collection's reset event (because a programmer six months later didn't notice the connection).
Now when the first collection gets reset, the moved model gets destroyed. Oops!
Iterating over the collection probably is the best way to handle this if you don't have an endpoint on your API that will delete all objects in a collection in batch on the API server (which is often how this is handled).
Luckily, that iteration is pretty easy:
destroyAll: function () {
var promises = [];
while(this.models.length > 0) {
promises.push( this.models[0].destroy() );
}
// handle errors communicating with the server
$.when(promises).fail(function (response) {
this.trigger('syncError',
response);
}.bind(this));
}
What you are looking for is, probably, for the models to be garbage-collected. That is, that nobody has a reference to these models anymore, after they are removed from the collection.
Backbone does its part of removing the references that it set on the models, when they are removed from the collection. However, you have to do your own cleanup if your code has references to those models. Most of the time, this happens if those models are registered as event listeners, like in this example: http://jsfiddle.net/dira/4uxp4/2/
Taking a look at the implementation of reset, you could change _removeReference to call a cleanup function on the model as well. And in the model, remove the model from all the listeners/all the other objects that keep a reference to it.

Backbone.js model validation only on set->save (not on fetch)

Caveat: I'm working with a backend that I don't have full control over, so I'm wrestling with a few considerations within Backbone that might be better addressed elsewhere...unfortunately, I have no choice but to handle them here!
So, my problem is that I'd really like to validate user input from a form (when I set values with it on Backbone models), but the models I receive from the API on newly created objects (via posts that ONLY accept a name, and ONLY return a name and object id) will fail my validation checks.
As example, when a new object is created in the database, two key fields are populated as empty strings (and so when Backbone hits the API and populates the models, it populates those keys with empty strings). When the user saves these objects back, post-edit, I'd like to force them to enter values for these two keys -- which is very easy, given Backbone's built in validation method.
The problem, of course, is that the validation is firing on both fetch->set (unwanted behavior) and set->save (desired behavior) -- and so newly created models won't load at all...Backbone collects them, validation fails, and errors fire.
So, my question is: is there a "Backbone-y" way to only validate the models on set->save, not on fetch->set? Could I use a specific trigger to work around this?
Any ideas would be greatly appreciated.
Backbone.Model.set won't perform validation if you pass in { silent: true }, and fetch will pass any options through to set, so you could either override fetch or write your own fetchSilent method that passes that in an options object.
However, you might run into a slight gotcha with Backbone.Collection.fetch, because when it receives attributes from the server, it doesn't create the new models with set. Instead, it creates a new model with model = new this.model(attrs, {collection: this}); and then performs validation if there's a validate method on the object.
This is a little annoying. You can get around it by defining a parse method on your collection (if you're using one) that creates a model silently (using {silent: true}), because when Backbone.Collection.add receives a fully formed Backbone model, it won't run the validation. (see the _add and _prepareModel methods in the annotated source).
It's a little annoying that the collection works that way, but (for now at least) it is what it is.
Instead of overriding fetch you can do another thing:
When you validate your model, check for model.silent and only validate if that doesn't exist.
So you do the following when you want to fetch a model:
var test = new MyModel({ id: '123', silent: true });
// in your Model validate function
validate: function(attrs) {
if (!attrs.silent) {
// validate logic here
}
}
Then you can fetch the model. After you get your model you can unset silent, for future validation.

Resources