Backbone - Render collection - javascript

I'm trying to make an application with Backbonejs and this is the first time I use a Front-end Javascript framework, except for JQuery.
I didn't yet understand how the rendering works.
My Example:
render: function() {
var events = this.collection.fetch({
success: function (model, response) {
console.log("Response is " + response);
var events = model.toJSON();
console.log(events.length);
console.log(model.toJSON());
return model.toJSON();
},
error: function(){
console.log("Errore during data fetch");
}
});
this.$el.html(this.template({events:this.collection.toJSON()}));
console.log("Event list: " + events.length);
},
The code above is the render callback of my view.
Inside the success collection fetch I get the data in json format from my API and I successfully log it on the console, but outside the fetch I don't have this data anymore and my view collection seems to be just an empty Backbone object.
Can somebody explain what I'm doing wrong and how rendering works ?
Enrico :)

Well, what render basically does it just inject some html code into the view's el (element).
It could be done by using template engine such as handlebars or mustache or just using the generic backbone (actually underscore) template, as you did in your code.
What we usually do, is initializing the view the way that it listens to the model it is attached to, or collection in your case.
It is not a very good practice to fetch data within render, as it should only use the already fetched data.
So what you could do, is to initialize your view this way:
var View = Backbone.View.extend({
initialize : function(){
this.listenTo(this.collection, "change", this.render);
},
redner : function(){
this.$el.html(this.template({events:this.collection.toJSON()}));
}
});
In this code, the view listens to changes occurring on the collection attached to it. When I say changes, I mean changes to any of the models in the collection, including when the collection is fetched for the first time.
So the listenTo method will fire the render method everytime there's a change in your collection, and by that the whole view will re-render.
This obviously happens when the collection is fetched for the first time.
And for the fetching itself? once youv'e binded the change event to the view, you can fetch it anywhere in your app, not necessarily inside the view.

Related

Backbone running a method based on a collection listener

In my application have the following code,
initialize: function() {
Pops.Collections.TeamCollection = this.collection;
this.collection.fetch();
this.collection.on('sync', this.render, this);
},
render: function() {
this.addAll();
return this;
},
Its pretty self explanatory, fetch the collection, once it is synced with the server run the render collection. At the time of writing this sequence of code it seemed like a a good idea however it now looks like that when I save a model of the collection it runs the sync listener and runs render again. This is not the behaviour I want. Is there another listener I can use to listen for the initial fetch being complete?
According to the backbone documentation,
Backbone.sync is the function that Backbone calls every time it attempts to read or save a model to the server.
The event is essentially the "catch-all" for any CRUD operations communicating with the server, which is why it's being fired on saving a model. Looking deeper into the documentation gives clues as to how .fetch() works
When the model data returns from the server, it uses set to (intelligently) merge the fetched models, unless you pass {reset: true}, in which case the collection will be (efficiently) reset.
By revising your call to this.collection.fetch({reset: true}), the collection will load the data and fire a reset event that can be listened to instead of sync. This will solve your problem.

The proper way of binding Backbone.js async fetch operation results to a view

I am wondering if there are any pointers on the best way of "fetching" and then binding a collection of data to a view within Backbone.js.
I'm populating my collection with the async fetch operation and on success binding the results to a template to display on the page. As the async fetch operation executes off the main thread, I one loses reference to the backbone view object (SectionsView in this case). As this is the case I cannot reference the $el to append results. I am forced to create another DOM reference on the page to inject results. This works but I'm not happy with the fact that
I've lost reference to my view when async fetch is executed, is there a cleaner way of implementing this ? I feel that I'm missing something...Any pointers would be appreciated.
SectionItem = Backbone.Model.extend({ Title: '' });
SectionList = Backbone.Collection.extend({
model: SectionItem,
url: 'http://xxx/api/Sections',
parse: function (response) {
_(response).each(function (dataItem) {
var section = new SectionItem();
section.set('Title', dataItem);
this.push(section);
}, this);
return this.models;
}
});
//Views---
var SectionsView = Backbone.View.extend(
{
tagName : 'ul',
initialize: function () {
_.bindAll(this, 'fetchSuccess');
},
template: _.template($('#sections-template').html()),
render: function () {
var sections = new SectionList();
sections.fetch({ success: function () { this.SectionsView.prototype.fetchSuccess(sections); } }); //<----NOT SURE IF THIS IS THE BEST WAY OF CALLING fetchSuccess?
return this;
},
fetchSuccess: function (sections) {
console.log('sections ' + JSON.stringify(sections));
var data = this.template({
sections: sections.toJSON()
});
console.log(this.$el); //<-- this returns undefined ???
$('#section-links').append(data); //<--reference independent DOM div element to append results
}
}
);
darthal, why did you re-implement parse? It seems to do exactly what Backbone does by default (receive an array of models from the AJAX call and create the models + add them to the collection).
Now on to your question... you are supposed to use the reset event of the Collection to do the rendering. You also have an add and remove when single instances are added or deleted, but a fetch will reset the collection (remove all then add all) and will only trigger one event, reset, not many delete/add.
So in your initialize:
this.collection.on("reset", this.fetchSuccess, this);
If you are wondering where the this.collection is coming from, it's a param you need to give to your view when you create it, you can pass either a model or a collection or both and they will automatically be added to the object (the view)'s context. The value of this param should be an instance of SectionList.
You'll also have to update fetchSuccess to rely on this.collection instead of some parameters. Backbone collections provide the .each method if you need to iterate over all the models to do stuff like appending HTML to the DOM.
In most cases you don't need a fetchSuccess, you should just use your render: when the collection is ready (on 'reset'), render the DOM based on the collection.
So to summarize the most general pattern:
The collection should be independent from the view: you give the collection as a param to the view creation, the collection shouldn't be created from a specific view.
You bind the View to the collection's reset event (+add, remove if you need) to run a render()
this.collection.on("reset", this.render, this);
You do a fetch on the collection, anytime (probably when you init your app).
A typical code to start the app would look something like this:
var sections = new SectionList();
var sectionsView = new SectionsView({collection: sections});
sections.fetch();
Because you bound the reset event in the view's initialize, you don't need to worry about anything, the view's render() will run after the fetch.

Backbone View render: fetch-first versus render-first, both approaches have faults?

This is more of a conceptual/architectural question than anything; the typical/popular approach to constructing and instantiating Backbone Views seems to be to only render the View AFTER successfully fetching necessary Model/Collection data from the server (in a success() or done() callback).
This is all well and good, but what if you have some sort of loading indicator or UI element within the View's template that needs to be displayed before/during the fetch? By not rendering the View until the call finishes, you effectively are unable to display such notifications to the user.
Conversely, if you render the View BEFORE making the fetch, you're now able to display such UI elements, but you now run the risk of displaying a mostly-empty template, since your Model/Collection data hasn't been retrieved yet, and this can look rather weird.
What if you need both things: UI notifications before/during the fetch, AND not to render a mostly-empty template pre-fetch? What might be some good approaches towards accomplishing this goal?
I use a separate loading view to handle the loading gif that listens for relevant events I trigger from the model/collection fetching results. The view responsible for rendering does so without being concerned about the loading gif. The loading gif essentially obscures the results which are "revealed" when the loading gif is removed:
var MyView = Backbone.View.extend({
initialize: function () {
this.listenTo(this.collection, 'reset', this.render);
},
render: function () {
// do whatever you need to here
}
});
var LoadingView = Backbone.View.extend({
el: '#loading',
initialize: function () {
this.listenTo(this.collection, 'fetchStarted', this.showLoader);
this.listenTo(this.collection, 'fetchFinished', this.hideLoader);
},
showLoader: function () {
var self = this;
if (this.waitHandle) {
clearTimeout(this.waitHandle);
delete this.waitHandle;
}
this.waitHandle = setTimeout(function () {
self.$el.removeClass('hide');
}, 300);
},
hideLoader: function () {
clearTimeout(this.waitHandle);
delete waitHandle;
this.$el.addClass('hide');
}
});
var MyCollection = Backbone.Collection.extend({
getResults: function () {
var self = this;
this.fetch({
beforeSend: function () {
self.trigger('fetchStarted');
},
complete: function () {
self.trigger('fetchFinished');
}
});
}
});
I use the timeouts in the loading view to delay showing the loading gif in case they aren't necessary.
I usually have an application-level view that will show a loader when it knows one of the app's main collections is being fetched. In face I usually do an ajax before filter to always pop the loader, and hide on completion application wide. It blocks the whole UI, but that's fine unless I'm doing a more complex dashboard-type app where there are clear modular concerns.
I wrote a blog post with a solution that could interest you (http://davidsulc.com/blog/2013/04/01/using-jquery-promises-to-render-backbone-views-after-fetching-data/). Basically, one approach to the problem:
Store the return value of the fetch call (which is a jQuery promise)
Render the loading view (with MyApp.myRegion.show(loadingView);)
If you want, you can already instantiate your view (that requires data) at this point
When the promise is fulfilled (i.e. the fetch is done), display your data-dependent view within the region (as above)

Backbone model update not propagating to collection

I have a basic backbone collection of models.
The view I'm working within displays information about the model, allowing edits.
In the render of my view I capture the model based on a passed in 'id'.
render: function() {
this.model = myCollection.get(this.options.passedInId);
// do the render...
}
I then have a click event which updates the model and calls the render to re-render with the updates
updateModel: function() {
var me = this;
this.model.set('someFlag', true);
this.model.save(this.model.toJSON(), {
success: function(model, resp) {
me.render();
}
}
My problem is when it comes back through the render the second time the get from the collection returns a different instance of the model (I can see a different cId on it) which does not contain my changed "someFlag" property. Therefore my edits don't show up when the view is re-rendered. I know there might be a more effecient way of handling this but my question is why does this occur? Shouldn't the model fetched from the collection include the edits I made on that model?
Only other thing is the "myCollection" in this example may have been reset between the initial get and the next get after the edit, but the id is still present and it finds a model just one without any of the updates.
My issue was the collection was being reset between the render method and the updateModel method.
This causes the model to get out of sync with the collection to correct the problem all I needed to do was bind on the reset and make sure my model gets updated with the "new" version. I added this to my render.
var me = this;
this.collection.on('reset', function () {
me.model = this.get(me.model.id);
};

BackboneJS Rendering Problems

For the last six months I've been working with Backbone. The first two months were messing around, learning and figuring out how I want to structure my code around it. The next 4 months were pounding away a production-fit application. Don't get me wrong, Backbone has saved me from the thousands-lines mess of client side code that were the standard before, but it enabled me to do more grandiose things in less time, opening up a complete new stack of problems. For all the questions I raise here there are simple solutions that feels like hacks or just feel wrong. I promise a 300 points bounty for an awesome solution. Here goes:
Loading - For our use case (an admin panel) pessimistic syncing is bad. For some things I need to validate things on the server before accepting them. We started out before the 'sync' event was merged into Backbone,
and we used this little code for mimicking the loading event:
window.old_sync = Backbone.sync
# Add a loading event to backbone.sync
Backbone.sync = (method, model, options) ->
old_sync(method, model, options)
model.trigger("loading")
Great. It works as expected but it doesn't feel correct. We bind this event to all the relevant views and display a loading icon until we receive a success or error event from that model. Is there a better, saner, way to do this?
Now for the hard ones:
Too many things render themselves too much - Let's say our application have tabs. Every tab controls a collection. On the left side you get the collection. You click a model to start editing it at the center. You change its name and press tab to get to the next form item. Now, your app is a "real time something something" that notices the difference, runs validations, and automatically sync the change to the server, no save button required! Great, but the H2 at the start of the form is the same name as in the input - you need to update it. Oh, and you need to update the name on the list to the side. OH, and the list sorts itself by names!
Here's another example: You want to create a new item in the collection. You press the "new" button and you start filling out the form. Do you immediately add the item to the collection? But what happens if you decided to discard it? Or if you save the entire collection on another tab? And, there's a file upload - You need to save and sync the model before you can start uploading the file (so you can attach the file to the model). So everything starts rendering in tremors: You save the model and the list and the form renders themselves again - it's synced now, so you get a new delete button, and it shows in the list - but now the file upload finished uploading, so everything starts rendering again.
Add subviews to the mix and everything starts looking like a Fellini movie.
It's subviews all the way down - Here's a good article about this stuff. I could not, for the love of everything that is holy, find a correct way to attach jQuery plugins or DOM events to any view that has subviews. Hell ensues promptly. Tooltips hear a render coming a long and start freaking around, subviews become zombie-like or do not respond. This is the main pain points as here actual bugs stand, but I still don't have an all encompassing solution.
Flickering - Rendering is fast. In fact, it is so fast that my screen looks like it had a seizure. Sometimes it's images that has to load again (with another server call!), so the html minimizes and then maximizes again abruptly - a css width+height for that element will fix that. sometimes we can solve this with a fadeIn and a fadeOut - which are a pain in the ass to write, since sometimes we're reusing a view and sometimes creating it anew.
TL;DR - I'm having problems with views and subviews in Backbone - It renders too many times, it flickers when it renders, subviews detach my DOM events and eat my brains.
Thank you!
More details: BackboneJS with the Ruby on Rails Gem. Templates using UnderscoreJS templates.
Partial rendering of views
In order to minimize the full rendering of your DOM hierarchy, you can set up special nodes in your DOM that will reflect updates on a given property.
Let's use this simple Underscore template, a list of names:
<ul>
<% _(children).each(function(model) { %>
<li>
<span class='model-<%= model.cid %>-name'><%= model.name %></span> :
<span class='model-<%= model.cid %>-name'><%= model.name %></span>
</li>
<% }); %>
</ul>
Notice the class model-<%= model.cid %>-name, this will be our point of injection.
We can then define a base view (or modify Backbone.View) to fill these nodes with the appropriate values when they are updated:
var V = Backbone.View.extend({
initialize: function () {
// bind all changes to the models in the collection
this.collection.on('change', this.autoupdate, this);
},
// grab the changes and fill any zone set to receive the values
autoupdate: function (model) {
var _this = this,
changes = model.changedAttributes(),
attrs = _.keys(changes);
_.each(attrs, function (attr) {
_this.$('.model-' + model.cid + '-' + attr).html(model.get(attr));
});
},
// render the complete template
// should only happen when there really is a dramatic change to the view
render: function () {
var data, html;
// build the data to render the template
// this.collection.toJSON() with the cid added, in fact
data = this.collection.map(function (model) {
return _.extend(model.toJSON(), {cid: model.cid});
});
html = template({children: data});
this.$el.html(html);
return this;
}
});
The code would vary a bit to accommodate a model instead of a collection.
A Fiddle to play with http://jsfiddle.net/nikoshr/cfcDX/
Limiting the DOM manipulations
Delegating the rendering to the subviews can be costly, their HTML fragments have to be inserted into the DOM of the parent.
Have a look at this jsperf test comparing different methods of rendering
The gist of it is that generating the complete HTML structure and then applying views is much faster than building views and subviews and then cascading the rendering. For example,
<script id="tpl-table" type="text/template">
<table>
<thead>
<tr>
<th>Row</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<% _(children).each(function(model) { %>
<tr id='<%= model.cid %>'>
<td><%= model.row %></td>
<td><%= model.name %></td>
</tr>
<% }); %>
</tbody>
</table>
</script>
var ItemView = Backbone.View.extend({
});
var ListView = Backbone.View.extend({
render: function () {
var data, html, $table, template = this.options.template;
data = this.collection.map(function (model) {
return _.extend(model.toJSON(), {
cid: model.cid
});
});
html = this.options.template({
children: data
});
$table = $(html);
this.collection.each(function (model) {
var subview = new ItemView({
el: $table.find("#" + model.cid),
model: model
});
});
this.$el.empty();
this.$el.append($table);
return this;
}
});
var view = new ListView({
template: _.template($('#tpl-table').html()),
collection: new Backbone.Collection(data)
});
http://jsfiddle.net/nikoshr/UeefE/
Note that the jsperf shows that the template can be be split into subtemplates without too much penalty, which would allow you to provide a partial rendering for the rows.
On a related note, don't work on nodes attached to the DOM, this will cause unnecessary reflows. Either create a new DOM or detach the node before manipulating it.
Squashing zombies
Derick Bailey wrote an excellent article on the subject of eradicating zombie views
Basically, you have to remember that when you discard a view, you must unbind all listeners and perform any additional cleanup like destroying the jQuery plugin instances. What I use is a combination of methods similar to what Derick uses in Backbone.Marionette:
var BaseView = Backbone.View.extend({
initialize: function () {
// list of subviews
this.views = [];
},
// handle the subviews
// override to destroy jQuery plugin instances
unstage: function () {
if (!this.views) {
return;
}
var i, l = this.views.length;
for (i = 0; i < l; i = i + 1) {
this.views[i].destroy();
}
this.views = [];
},
// override to setup jQuery plugin instances
stage: function () {
},
// destroy the view
destroy: function () {
this.unstage();
this.remove();
this.off();
if (this.collection) {
this.collection.off(null, null, this);
}
if (this.model) {
this.model.off(null, null, this);
}
}
});
Updating my previous example to give the rows a draggable behavior would look like this:
var ItemView = BaseView.extend({
stage: function () {
this.$el.draggable({
revert: "invalid",
helper: "clone"
});
},
unstage: function () {
this.$el.draggable('destroy');
BaseView.prototype.unstage.call(this);
}
});
var ListView = BaseView.extend({
render: function () {
//same as before
this.unstage();
this.collection.each(function (model) {
var subview = new ItemView({
el: $table.find("#" + model.cid),
model: model
});
subview.stage();
this.views.push(subview);
}, this);
this.stage();
this.$el.empty();
this.$el.append($table);
return this;
}
});
http://jsfiddle.net/nikoshr/yL7g6/
Destroying the root view will traverse the hierarchy of views and perform the necessary cleanups.
NB: sorry about the JS code, I'm not familiar enough with Coffeescript to provide accurate snippets.
Ok, in order.. :)
Loading...
In case you want to validate data which stored on server, good practice do it on server-side. If validation on server will be unsuccessful, server should send not 200 HTTP code, therefore save metod of Backbone.Model will trigger error.
Other side, for validation data backbone has unimplemented validate method. I guess that right choise to implement and use it. But keep in mind that validate is called before set and save, and if validate returns an error, set and save will not continue, and the model attributes will not be modified. Failed validations trigger an "error" event.
Another way, when we call silent set(with {silent: true} param), we should call isValid method manually to validate data.
Too many things render themselves too much..
You have to separate your Views under their logic. Good practice for collection is separate view for each model. In this case you could render each element independently. And even more - when you initalizing your container view for collection, you could bind any event from each model in the collection to appropriate view, and they will render automatically.
Great, but the H2 at the start of the form is the same name as in the
input - you need to update it. Oh, and you need to update the name on
the list to the side.
you could use JQuery on method to implement callback which send value to display. Example:
//Container view
init: function() {
this.collection = new Backbone.Collection({
url: 'http://mybestpage.com/collection'
});
this.collection.bind('change', this.render, this);
this.collection.fetch();
},
render: function() {
_.each(this.collection.models, function(model) {
var newView = new myItemView({
model: model,
name: 'view' + model.id
});
this.$('#my-collection').append(newView.render().$el);
view.on('viewEdit', this.displayValue);
}, this);
},
...
displayValue: function(value) {
//method 1
this.displayView.setText(value); //we can create little inner view before,
//for text displaying. Сonvenient at times.
this.displayView.render();
//method 2
$(this.el).find('#display').html(value);
}
//View from collection
myItemView = Backbone.View.extend({
events: {
'click #edit': 'edit'
},
init: function(options) {
this.name = options.name;
},
...
edit: function() {
this.trigger('viewEdit', this.name, this);
}
OH, and the list sorts itself by names!
You can use sort method for backbone collections. But (!) Calling sort triggers the collection's "reset" event. Pass {silent: true} to avoid this. How to
Here's another example: You want to create a new item in the
collection...
When we press a "New" button we need to create a new model, but only when .save() method will trigger success, we should push this model to collection. In another case we should display error message. Of course we have no reasons to add a new model to our collection until it has been validated and saved on server.
It's subviews all the way down... subviews become zombie-like or do not respond.
when you (or any model) calling render method, all elements inside it will be recreated. So in case when you have subviews, you should call subView.delegateEvents(subView.events); for all of subviews; Probably this method is little trick, but it works.
Flickering..
Using thumbnails for big and medium images will minimize flickering in lot of cases. Other way, you could separate rendering of view to images and other content.
Example:
var smartView = Backbone.View.extend({
initialize: function(){
this.model.on( "imageUpdate", this.imageUpdate, this );
this.model.on( "contentUpdate", this.contentUpdate, this );
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
},
imageUpdate: function(){
this.$el.find('#image').attr('src', this.model.get('imageUrl'));
},
contentUpdate: function(){
this.$el.find('#content').html(this.model.get('content'));
}
})
I hope this helps anyone. Sorry for grammar mistakes, if any :)
Loading...
I'm a huge fan of eager loading. All my server calls are JSON responses, so it isn't a huge deal to make them more often than not. I usually refresh a collection every time it's needed by a view.
My favorite way to eager load is by using Backbone-relational. If I organize my app in a hierarchical manner. Consider this:
Organization model
|--> Event model
|--> News model
|--> Comment model
So when a user is viewing an organization I can eager load that organization's events and news. And when a user is viewing a news article, I eager load that article's comments.
Backbone-relational provides a great interface for querying related records from the server.
Too many things render themselves too much...
Backbone-relational helps here too! Backbone-relational provides a global record store that proves to be very useful. This way, you can pass around IDs and retrieve the same model elsewhere. If you update it in one place, its available in another.
a_model_instance = Model.findOrCreate({id: 1})
Another tool here is Backbone.ModelBinder. Backbone.ModelBinder lets you build your templates and forget about attaching to view changes. So in your example of collecting information and showing it in the header, just tell Backbone.ModelBinder to watch BOTH of those elements, and on input change, your model will be updated and on model change you view will be updated, so now the header will be updated.
It's subviews all the way down... subviews become zombie-like or do not respond...
I really like Backbone.Marionette. It handles a lot of the cleanup for you and adds an onShow callback that can be useful when temporarily removing views from the DOM.
This also helps to facilitate attaching jQuery plugins. The onShow method is called after the view is rendered and added to the DOM so that jQuery plugin code can function properly.
It also provides some cool view templates like CollectionView that does a great job of managing a collection and its subviews.
Flickering
Unfortunately I don't have much experience with this, but you could try pre-loading the images as well. Render them in a hidden view and then bring them forward.

Categories

Resources