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...
Related
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.
What is a general good practice for when some action - changes multiple models in Backbone.js:
Trigger multiple PUT requests for each mode.save()
Single request to sync the entire collection
In case if the quantity of the changed models greater than 1 - definitely it should be the second item.
Usually, good REST api practice seems to suggest that you should update, save, create, delete single instances of persistent elements. In fact, you will find that a Backbone.Collection object does not implement these methods.
Also, if you use a standard URI scheme for your data access point, you will notice that a collection does not have a unique id.
GET /models //to get all items,
GET /models/:id //to read an element,
PUT /models/:id //to update an element,
POST /models/:id //to create an element,
DELETE /models/:id //to delete an element.
If you need to update every model of a collection on the server at once, maybe you need to ask why and there might be some re-thinking of the model structure. Maybe there should be a separate model holding that common information.
As suggested by Bart, you could implement a PATCH method to update only changed attributes of a particular element, thus saving bandwidth.
I like the first option, but I'd recommend you implement a PATCH behavior (only send updated attributes) to keep the requests as small as possible. This method gives you a more native "auto-save" feel like Google Docs. Of course, this all depends on your app and what you are doing.
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 ;)).
A collection knows its models, but is there a way for the model to get to its collection?
I would need a way to get to the "adjacent" models in a collection. The following may give a better idea of the problem:
I'm trying to implement a simple app that ranks items based on an "priority" attribute in the model and maintains a list of them. Each model view has buttons for increasing or decreasing the priority, which should alter the ordering of the list accordingly.
Simply adding or subtracting one from the current value will work as long as the priority-attributes are continuous, for example:
1,2,3,4,5,6
But there is also a delete button, which allows deleting any model from the list. It can lead to priority-attributes like:
1,2,5,6
Now changing the priority of 5 to 4 wont change the order of the list as it's still greater than 2.
It's undocumented, but the models will automatically have a collection property that points to the collection. I don't know the details of how that works if a model is in multiple collections (perhaps the collection property is not changed once it's set).
To solve the specific problem you mentioned, you can listen for remove events on the collection and adjust things accordingly. In this case you don't even need that collection property, because both the model and collection will be passed to the remove listener.
var handler = function ( model, collection, options ) {
// ...
};
collection.on( 'remove', handler );
The Backbone.Collection docs also say this:
the index at which the model is being removed from the collection is available as options.index
The options argument to remove listeners is not documented where the signature for remove listeners is documented, but it is passed (in 0.9.2 anyway).
I'm using Backbone.js 0.9.2 from the Backbone-on-rails gem. I'm also trying to use the new 'pushState' instead of the old hash URL.
The Problem
I'm building a standard Rails like CRUD interface to keep track of my appointments. I have a 'new' link on the main index.jst.eco page:
<h1>Appointments</h1>
<p>New Appointment</p>
I load the page and click on that 'new' link and backbone fires off the event and doesn't have to reload the whole page. Here is that event:
class BackboneOnRails.Views.AppointmentsIndex extends Backbone.View
template: JST['appointments/index'],
events: ->
'click .new': 'newAppointment'
newAppointment: ->
Backbone.history.navigate("/appointments/new", {trigger: true})
return false
# The rest of the index methods omitted for brevity
This then invokes the backbone router:
class BackboneOnRails.Routers.Appointments extends Backbone.Router
routes:
'': 'index'
'appointments': 'index'
'appointments/new': 'new'
initialize: ->
this.appointments = new BackboneOnRails.Collections.Appointments()
this.appointmentsIndexView = new BackboneOnRails.Views.AppointmentsIndex({collection: this.appointments})
this.appointmentsIndexView.render()
index: ->
$("#container").html(this.appointmentsIndexView.el)
this.appointments.fetch()
new: ->
appointments = new BackboneOnRails.Collections.Appointments()
view = new BackboneOnRails.Views.AppointmentNew({collection: appointments})
$("#container").html(view.render().el)
The problem happens when I hit the browsers back button, then try using the 'new' link again. This time around it does a full reload of the page.
What is happening to the javascript bindings when I hit back on the browser?
I have a show event for the item and with that I can go back and forth no problem. I've compared both and they look like the same sort of calls.
The problem is in your attempted re-use of the appointmentsIndexView instance. Removing the view from the DOM destroys the DOM event handlers. Re-adding the view's el to the DOM does not re-connect them.
Overview Of The Problem
When you load that view with the initialize and index methods of your router the first time, everything is fine because you have a fresh instance of the IndexView. The DOM events are attached to the view properly, and life is good.
When you hit the new route / method of your router, you're effectively trying to remove the index view from the screen and replace it with the add new view. This works from a visual stand point and from the standpoint of the add new view.
When you hit the back button, though, you're staying within the same live application instance in your browser tab. Hitting the back button with pushstate enabled tells the browser not to reload the entire app, just to update the url and fire off the router method for the index.
In this case, you're index view is not re-built from the ground up. You're re-using the same view instance, but re-loading it with data from the server. The data load works perfectly fine because your view and collection are still attached. The DOM event bindings fail, however, because they bindings were previously removed and not re-added.
2 Common Solutions
There are two common solutions for this, and many variations of these solutions.
1) Don't re-use view instances.
This is my strongly recommended suggestion. In every instance where I have tried to re-use a view instance, I have consistently run into very large problems - including the exact problem you're having.
Instead, re-create a new view instance every time you need to show the index of appointments. That means you create the new index view in your index method of the router, instead of the initialize method.
2) Clear and re-bind the DOM events
If, for some reason, you feel that you really need to re-use the view instance (which should never be true), you can solve it with some information that Tim Branyen posted on his blog a while back:
http://tbranyen.com/post/missing-jquery-events-while-rendering
I do not recommend this approach. Re-using a view instance might seem like a good idea off-hand, but it will lead down a bad path toward other problems, including bloated memory usage by leaving too many unused parts around in your app.
Side Note: Zombies And Memory Leaks
In either case - whether you decide to re-use view instances or re-create them when you need them - you're likely to run in to some memory leaks.
In the case of re-using a view, you're explicitly holding on to an object in memory when you don't need to. This isn't really a "leak" but it's an excessive use of memory. You should de-reference the object when it's not needed and re-create it when it is needed. This will cut down on memory usage and allow your app to perform better.
I have a blog post covering how this works, here: http://lostechies.com/derickbailey/2012/03/19/backbone-js-and-javascript-garbage-collection/
In the case of not re-using a view, you may wind up with a true memory leak by leaving model and collection event bindings hanging around after a view has been removed from the visible DOM. If you decide not to re-use your views, you will need to make your code that replaces the #container html more robust, and have it clean up the old view.
I've got a blog post detailing a solution for that, as well: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/ - be sure to read the comments from Johnny Oshika in this post, as he points to a very useful StackOverflow answer where he shows a simple method of handling model and collection events.