BackboneJS Rendering Problems - javascript

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.

Related

Backbone - Render collection

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.

At which point does one fetch a Backbone Model on which a view depends?

I seem to often end up in a situation where I am rendering a view, but the Model on which that view depends is not yet loaded. Most often, I have just the model's ID taken from the URL, e.g. for a hypothetical market application, a user lands on the app with that URL:
http://example.org/#/products/product0
In my ProductView, I create a ProductModel and set its id, product0 and then I fetch(). I render once with placeholders, and when the fetch completes, I re-render. But I'm hoping there's a better way.
Waiting for the model to load before rendering anything feels unresponsive. Re-rendering causes flickering, and adding "loading... please wait" or spinners everywhere makes the view templates very complicated (esp. if the model fetch fails because the model doesn't exist, or the user isn't authorized to view the page).
So, what is the proper way to render a view when you don't yet have the model?
Do I need to step away
from hashtag-views and use pushState? Can the server give me a push? I'm all ears.
Loading from an already-loaded page:
I feel there's more you can do when there's already a page loaded as opposed to landing straight on the Product page.
If the app renders a link to a Product page, say by rendering a ProductOrder collection, is there something more that can be done?
<ul id="product-order-list">
<li>Ordered 5 days ago. Product 0 (see details)</li>
<li>Ordered 1 month ago. Product 1 (see details)</li>
</ul>
My natural way to handle this link-to-details-page pattern is to define a route which does something along these lines:
routes: {
'products/:productid': 'showProduct'
...
}
showProduct: function (productid) {
var model = new Product({_id: productid});
var view = new ProductView({model: model});
//just jam it in there -- for brevity
$("#main").html(view.render().el);
}
I tend to then call fetch() inside the view's initialize function, and call this.render() from an this.listenTo('change', ...) event listener. This leads to complicated render() cases, and objects appearing and disappearing from view. For instance, my view template for a Product might reserve some screen real-estate for user comments, but if and only if comments are present/enabled on the product -- and that is generally not known before the model is completely fetched from the server.
Now, where/when is it best to do the fetch?
If I load the model before the page transition, it leads to straightforward view code, but introduces delays perceptible to the user. The user would click on an item in the list, and would have to wait (without the page changing) for the model to be returned. Response times are important, and I haven't done a usability study on this, but I think users are used to see pages change immediately as soon as they click a link.
If I load the model inside the ProductView's initialize, with this.model.fetch() and listen for model events, I am forced to render twice, -- once before with empty placeholders (because otherwise you have to stare at a white page), and once after. If an error occurs during loading, then I have to wipe the view (which appears flickery/glitchy) and show some error.
Is there another option I am not seeing, perhaps involving a transitional loading page that can be reused between views? Or is good practice to always make the first call to render() display some spinners/loading indicators?
Edit: Loading via collection.fetch()
One may suggest that because the items are already part of the collection listed (the collection used to render the list of links), they could be fetched before the link is clicked, with collection.fetch(). If the collection was indeed a collection of Product, then it would be easy to render the product view.
The Collection used to generate the list may not be a ProductCollection however. It may be a ProductOrderCollection or something else that simply has a reference to a product id (or some sufficient amount of product information to render a link to it).
Fetching all Product via a collection.fetch() may also be prohibitive if the Product model is big, esp. in the off-chance that one of the product links gets clicked.
The chicken or the egg? The collection.fetch() approach also doesn't really solve the problem for users that navigate directly to a product page... in this case we still need to render a ProductView page that requires a Product model to be fetched from just an id (or whatever's in the product page URL).
Alright, so in my opinion there's a lot of ways that you can fix this. I'll list all that I've thought of and hopefully one will work with you or at the very minimum it will inspire you to find your optimal solution.
I'm not entirely opposed to T J's answer. If you just go ahead and do a collection.fetch() on all the products when the website is loading (users generally expect there to be some load time involved) then you have all of your data and you can just pass that data round like he mentioned. The only difference between what he's suggesting and what I normally do is that I usually have a reference to app in all my views. So, for example in my initialize function in app.js I'll do something like this.
initialize: function() {
var options = {app: this}
var views = {
productsView: new ProductsView(options)
};
this.collections = {
products: new Products()
}
// This session model is just a sandbox object that I use
// to store information about the user's session. I would
// typically store things like currentlySelectedProductId
// or lastViewedProduct or things like that. Then, I just
// hang it off the app for ease of access.
this.models = {
session: new Session()
}
}
Then in my productsView.js initialize function I would do this:
initialize: function(options) {
this.app = options.app;
this.views = {
headerView: new HeaderView(options),
productsListView: new ProductsListView(options),
footerView: new FooterView(options)
};
}
The subviews that I create in the initialize in productsView.js are arbitrary. I was mostly just trying to demonstrate that I continue to pass that options object to subviews of views as well.
What this does is allows every view, whether it be a top level view or deeply nested subview, every view knows about every other view, and every single view has reference to the application data.
These two code samples also introduce the concept of scoping your functionality as precise as you possibly can. Don't try to have a view that does everything. Pass functionality off to other views so that each view has one specific purpose. This will promote reuse of views as well. Especially complex modals.
Now to get back to the actual topic at hand. If you were going to go ahead and load all of the products up front where should you fetch them? Because like you said you don't want a blank page just sitting there in front of your user. So, my advice would be to trick them. Load as much of your page as you possibly can and only block the part that needs the data from loading. That way to the user the page looks like it's loading while you're actually doing work behind the scenes. If you can trick the user into thinking the page is steadily loading then they are much less likely to get impatient with the page load.
So, referencing the initialize from productsView.js, you could go ahead and let the headerView and footerView render. Then, you could do your fetch in the render of the productsListView.
Now, I know what you're thinking. Have I lost my mind. If you do a fetch in the render function then there's no way that the call will have time to return before we hit the line that actually renders the productsViewList template. Well, luckily there's a couple of ways around that. One way would be to use Promises. However, the way I typically do it is to just use the render function as its own callback. Let me show you.
render: function(everythingLoaded) {
var _this = this;
if(!!everythingLoaded) {
this.$el.html(_.template(this.template(this)));
}
else {
// load a spinner template here if you want a spinner
this.app.collection.products.fetch()
.done(function(data) {
_this.render(true);
})
.fail(function(data) {
console.warn('Error: ' + data.status);
});
}
return this;
}
Now, by structuring our render this way the actual template won't load until the data has fully loaded.
While we have a render function here I want to introduce another concept that I use every where. I call it postRender. This is a function where I execute any code that depends on DOM elements being in place once the template has finished loading. If you were just coding a plain .html page then this is code that traditionally goes in the $(document).ready(function() {});. It may be worth noting that I don't use .html files for my templates. I use embedded javascript files (.ejs). Continuing on, the postRender function is a function that I have basically added to my boiler plate code. So, any time I call render for a view in the code base, I immediately chain postRender onto it. I also use postRender as a call back for itself like I did with the render. So, essentially the previous code example would look something like this in my code base.
render: function(everythingLoaded) {
var _this = this;
if(!!everythingLoaded) {
this.$el.html(_.template(this.template(this)));
}
else {
// load a spinner template here if you want a spinner
this.app.collection.products.fetch()
.done(function(data) {
_this.render(true).postRender(true);
})
.fail(function(data) {
console.warn('Error: ' + data.status);
});
}
return this;
},
postRender: function(everythingLoaded) {
if(!!everythingLoaded) {
// do any DOM manipulation to the products list after
// it's loaded and rendered
}
else {
// put code to start spinner
}
return this;
}
By chaining these functions like this we guarantee that they'll run sequentially.
=========================================================================
So, that's one way to tackle the problem. However, you mentioned that you don't want to necessarily load all of the products up front for fear that the request could take too long.
Side Note: You should really consider taking out any information related to the products call that could cause the call to take a considerable amount of time, and make the larger pieces of information a separate request. I have a feeling that users will be more forgiving about data taking a while to load if you can get them the core information really fast and if the thumbnails related to each product takes a little longer to load it shouldn't be then end of the world. That's just my opinion.
The other way to solve this problem is if you just want to go to a specific product page then just implement the render/postRender pattern that I outlined above on the individual productView. However note that your productView.js will probably have to look something like this:
initialize: function(options) {
this.app = options.app;
this.productId = options.productId;
this.views = {
headerView: new HeaderView(options),
productsListView: new ProductsListView(options),
footerView: new FooterView(options)
};
}
render: function(everythingLoaded) {
var _this = this;
if(!!everythingLoaded) {
this.$el.html(_.template(this.template(this)));
}
else {
// load a spinner template here if you want a spinner
this.app.collection.products.get(this.productId).fetch()
.done(function(data) {
_this.render(true).postRender(true);
})
.fail(function(data) {
console.warn('Error: ' + data.status);
});
}
return this;
},
postRender: function(everythingLoaded) {
if(!!everythingLoaded) {
// do any DOM manipulation to the product after it's
// loaded and rendered
}
else {
// put code to start spinner
}
return this;
}
The only difference here is that the productId was passed along in the options object to the initialize and then that's pulled out and used in the .fetch in the render function.
=========================================================================
In conclusion, I hope this helps. I'm not sure I've answered all of your questions, but I think I made a pretty good pass at them. For the sake of this getting too long I'm going to stop here for now and let you digest this and ask any questions that you have. I imagine I'll probably have to do at least 1 update to this post to further flush it out.
You started saying:
I have a listing of items in one Collection view
So what does a collection have..? Models..!
When you do collection.fetch() you retrieve all the models.
When the user selects an item, just pass the corresponding model to the item view, something like:
this.currentView = new ItemView({
model: this.collection.find(id); // where this points to collection view
// and id is the id of clicked model
});
This way, there there won't be any delay/ improper rendering.
What if your collections end point returns huge volume of data..?
Then implement common practices like pagination, lazy loading etc.
I construct a Product model with the given ID
To me that sounds wrong. If you have a collection of products, you shouldn't be constructing such models manually.
Have the collection fetch your models before rendering the list view. This way all the problem you mentioned can be avoided.

How do I manage a Backbone app?

I am trying to wrap my head around Backbone, more specifically how the an application flows throughout it's life. Unfortunately at my job I do not have access (or say for that matter) on how our API is structured. We have many different calls from different time periods with crazy inconsistent structure.
Overriding fetch or sync is not a problem to standaraize the return but what I run into (at the very beginning of my dive in the a Backbone application) is a how to layout the actual code.
Here is my real world example. This page is non-critical and I am trying to re-write it with Backbone. Here is the flow:
Page loads a list of genre types from a call
Clicking on a genre type loads sub genres based off of the genre type (the sub genre type requres a genre code as the parameter)
Clicking on the sub genre type loads all products with that criteria.
I can get pretty far but at some point I feel the code is getting mangled - or doesn't feel natural. Like I am shoving things in.
So my official questions is: How do I manage a Backbone app?
Here is a summary of my though process:
I created a global namespace as one should
var App = App || {};
Okay, lets start with the main application view as all examples show:
App.MainView = Backbone.View.extend({
//this loads the outer stuff
//and creates an instance of the Genre View
});
Alright pretty straightforward, I am going to need a genre model, collection, and view (this applies to sub genre as well)
App.Genre = Backbone.Model.extend();
App.Genres = Backbone.Collection.extend({
url: 'returns a list of genres',
model: App.Genre,
initialize: function() {
this.fetch();
},
fetch: function() {
var self = this;
$.ajax({
url: this.url,
success: function(response) {
**format return**
self.add(formattedArrayOfModels);
}
});
}
});
Now on to the view, the confusing part
App.GenreView = Backbone.View.extend({
el: 'element',//easy enough
tmpl: 'my handlebars template',//implementing handlebars...no problem
initialize: function() {
//this produces a collection full of genres
this.genreList = new App.Genres();
this.genreList.on('add', _.bind(this.render, this));
},
render: function() {
//rendering not a problem, pretty straight forward
}
});
Up until here I have no problems. The genre list loads and we're good to go. So, now when the user clicks a genre I want it to load a sub genre
events: {
'click a': 'getSubGenres'
},
getSubGenres: function(e) {
}
Here is my problem. In getSubGenres do I keep it local?
var subGenre = new App.SubGenreView();
Or should I make it part of the Genre view?
this.subGenre = new App.SubGenreView();
Should I somehow put it in a parent object so it can be accessed by other views? How do I control things like that?
And if I already have a collection of sub genres how do I just use the loaded collection (instead of another ajax call).
Is this the approach you would use?
couple of things before I answer,
first: the fetch function doesn't need an $ajax call since it's its job, so, you can evaluate error:function(){} and success:function(){} immediately inside fetch, but that's assuming that the URL is set correctly.
second: one thing that helped me a lot in my backbone keyboard-head-fight is the addy osmani Backbone Fundamentals which contains a very rich tutorial in pdf format.
now back to the question: from my experience, you will mostly need 'this', so it's a good habbit to get used to it, plus there is something that solves a lot of these issues if implemented correctly: backbone layoutmanager
anyway, the decision of where to place the subview, is totally a design decision in your case and depends a lot on how you structure your page and files.
about how to use the "collection" that is preloaded: I really didn't get it, because the collection you're talking about contains all the subgenres, so usually it shouldn't change even if the view changes to a certain genre view, you are still able to use it.
but still everything I said, is relative to how you structure your files, I do an app.js and a router.js and lots of other files, but the main work is always on the main two, so basically I always get access to everything.
I hope this answered your question

backbone.js - unbinding and swapping views

I'm going through the process of learning Backbone.js and I've come across a few things that look like they work... but behind the scenes, they might be causing some problems. One such issue is design patterns for swapping views.
I have a menu on the left (and corresponding view) that contains a list of users. Each user, when clicked, will display their corresponding list of movies to the right into another view. I do this by getting the model of the clicked user, building a new Movie collection based on that model, and rendering a new Movie View (to the right). If I click on another user, it does the same thing: it gets the user model, builds a new Movie collection, and renders a new Movie View to the right, replacing the DOM element. This looks fine -- but I'm worried about all of the new objects/bindings that are being created, and potential issues that could arise. So, what are my alternatives?
(1) Should I be trying to have the view redraw when the collection changes? How do I do this if I'm creating new collections?
(2) Or should I be unbinding everything when another user is clicked?
Here is my userlist view:
Window.UsersList = Backbone.View.extend({
el: $("#users"),
initialize: function(){
this.collection.bind('reset', this.render, this);
},
render: function(){
var users = [];
this.collection.each(function(user){
users.push(new UsersListItem({model: user}).render().el);
},this);
$(this.el).html(users);
return this;
}
});
In my UsersListItem view I capture the click event and call show_user_movies in my controller:
show_user_movies: function(usermodel){
// fetchMovies() lazy-loads and returns a new collections of movies
var movie_collection = usermodel.fetchMovies();
movie_list = new MovieList({collection: movie_collection, model: usermodel});
movie_list.render();
},
Thanks for any suggestions.
Just re-use the same MovieList view along with it's associated collection, using reset(models) to update the models in the collection, which should re-render the view. You can use the same pattern you have above with your MovieList view binding to the collection's reset event and re-rendering itself at that time. Note that usermodel.fetchMovies() doesn't follow the backbone asynchronous pattern of taking success/error callbacks, so I don't think the code works as is (maybe you simplified for the purpose of this question), but the point is when the new set of models arrives from the server, pass it to movie_list.collection.reset and you're good to go. This way you don't have to worry about unbinding events and creating new views.
show_user_movies: function(usermodel){
// fetchMovies() lazy-loads and returns a new collections of movies
movie_list.collection.reset(usermodel.fetchMovies().models);
},

remove from collection bind to remove from view

I have a backbone collection and when I remove a model from the collection, I want it to remove the item from a list in the view.
My collection is pretty basic
MyApp.Collections.CurrentEvents = Backbone.Collection.extend({
model: MyApp.Models.Event
});
and in my views I have
MyApp.Views.CurrentEventItem = Backbone.View.extend({
el: 'div.current_event',
initialize: function(){
event = this.model;
_.bindAll(this, "remove");
MyApp.CurrentEvents.bind('remove',this.remove); //the collection holding events
this.render();
},
// yeah, yeah, render stuff here
remove: function(){
console.log(this);
$(this.el).unbind();
$(this.el).remove();
}
});
when I remove the model from the collection, it triggers the remove function, but the view is still on the page.
In the console, I can see the model, but I believe the model should have an 'el', but it doesn't.
My container code is
MyApp.Views.CurrentEventsHolder = Backbone.View.extend({
el: 'div#currentHolder',
initialize: function(){
MyApp.CurrentEvents = new MyApp.Collections.CurrentEvents();
MyApp.CurrentEvents.bind('new', this.add);
},
add: function(){
var add_event = new MyApp.Views.CurrentEventItem(added_event);
$('div#currentHolder').append(add_event.el);
}
});
for some reason in the add method I can't seem to use the $(this.el) before the append, though I'm not sure if that is the problem.
PROBLEM: MyApp.CurrentEvents.bind('remove',this.remove);
This triggers the remove() function every time any model is deleted from the collection.
This means that anytime a model is deleted, all the CurrentEventItem view instances will be deleted.
Now, about the view still being on the page:
It must have something to do with the way you appended/added/html-ed the view in the page. Check your other views, maybe if you have a CurrentEventsContainer view of some sort, check your code from there because with your current code, it does delete the view, albeit, all of them though.
RECOMMENDED FIX:
change your bindings to:
this.model.bind('remove',this.remove);
and make sure that when you instantiate it, pass on the model so that each view will have a corresponding model to it like so:
//...
addAllItem: function(){
_.each(this.collection, this.addOneItem())
},
addOneItem: function(model){
var currentEventItem = new MyApp.Views.CurrentEventItem({model:model});
//...more code..
}
//...
This makes things a lot easier to manage in my opinion.
UPDATE (from your updated question)
The reason you aren't able to use this.el is because you haven't passed the right context into the add() function. You need to add _.bindAll(this,'add') in your initialize() function to pass the right context, therefore making your this correct, allowing you to use this.el within the add function. Also, change your code to this:
MyApp.CurrentEvents.bind('new', this.add, this); this passes the right context. Also, why not use add instead as an event?
Continuing what I said in the comments, the way you've implemented this right now will remove all the CurrentEventItem views from the page when any of them is removed from the collection. The reason for this is the following:
MyApp.CurrentEvents.bind('remove',this.remove);
What this essentially says is, "every time the remove event is triggered on the collection, call this.remove." So, every time you instantiate one of these views, you're also telling the collection to remove that view when the collection triggers a remove event. I've created a fiddle to show you the problem.
You're right that Backbone knows which model has been removed from a collection, but you're not taking advantage of that. You can do that like so:
removeFromView: function(model) {
// Check to make sure the model removed was this.model
if (model.cid === this.model.cid) {
$(this.el).unbind();
$(this.el).remove();
}
}
See how this minor adjustment changes the behavior? Check it out in action here.
If you follow this pattern, you should see the proper views being removed.

Categories

Resources