I am using Backbone.js to render a list of items (email recipients) that have different status, eg. confirmed, pending and so on. After the list is rendered there are options for user to filter them so the user can list all recipients, or only confirmed recipients and so on. The items (recipients) are naturally stored in a collection..
My approach is to on a filter event:
Clear all item's view's
From the app view call a filterOnStatus function in the collection that return all models and adds them to the view.
Step 2 works fine. But what's the best way to clear all items on a collection's view's.
In the Todo example application (http://documentcloud.github.com/backbone/examples/todos/index.html) they do something similar. In the app view the following code is used to clear all completed items from the list.
clearCompleted: function() {
_.each(Todos.done(), function(todo){ todo.destroy(); });
return false;
},
The difference here is that they do this by removing the actual model. And that model's view listens for the destroy event which them removes the view.
I want to keep the model.
What's the best way to solve this. Do I in the models need to stored a reference to its views and then iterate over the models and remove the views?
Is there a better approach if I want to filter on attributes in the models?
If your first step is simply clearing all items then why don't you add a simple method to your AppView that will do just that, like: clearList: function() { this.$('.list').html('') }.
Or even better, you can filter all models and render them to the temporarily element and than replace current list with it. Thus all the filtering will be done in only one DOM call (DOM is slow). Example with jQuery:
AppView.filterOnStatus = function() {
var $fragment = $('<div/>')
// filter your collection and append rendered views to $fragment
this.$('.list').html( $fragment.html() )
}
Of course there are more complicated ways, but whether you need them depends on what you're trying to achieve. From what I understood this simple approach would be fine enough.
Related
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.
Using Ember, we have a list of shoes which is fetched from a database. These are listed at '/shoes.
this.resource('shoes', function() {
this.route('new');
this.route('show', {path: ':shoe_id'});
this.route('edit', {path: ':shoe_id/edit'});
});
Only the first 10 shoes in the MongoDB collection are listed in the view, as specified in our webb API. When creating a new shoe (using the nested route 'new'), and transitioning back to '/shoes', the new shoe is added to the current 'shoes' model.
export default Ember.ObjectController.extend({
actions: {
save: function() {
this.get('model').save();
this.transitionToRoute('shoes');
}
}
});
This results in a list of 11 shoes. In other words, it does not use the route and make a new API call. Instead, it is added to the current list of shoes in the model. When refreshing the page, the result is rendered as intended, fetching the 10 first records of the DB collection.
We would like to make the ’transitionToRoute’ execute the route and re-fetch the model instead of just adding it to the current model. We have seen a few examples of how ’this.refresh()’ and ’this.reload()’ can be used inside the controller's 'model' scope body but these examples have not worked for us.
Is it possible to make a ’transitionToRoute’ refresh the model with new database values using the 'shoes' route?
Based on what you wrote, I'm guessing you're trying to use pagination and only want the first 10 shoes to be listed on your /shoes route?
If so, the "Ember Way" is to always keep all your models in sync and never have to do special work just to get the view to update artificially. In this case, Ember has a local store of shoes where it initially has 10 items. Then you add one more, it gets saved both the database and to the Ember local store and so now Ember thinks (correctly) that you have 11 shoes. Just because Mongo returns 10 shoes doesn't mean your entire data set is 10 shoes.
So, the best way to handle this situation is to have your view display an accurate projection of your underlying model data. In other words, don't tell your view to display "all shoes". Tell it to display a "filtered list of all shoes"!
In practice, I've seen two types of filtering on ArrayController. One is just to return the first n values. For that use good old javascript slice (See MDN docs). The second is to use the Ember filter function. See Ember Docs.
Ultimately, your controller would something like this:
Shoes Controller:
export default Ember.ArrayController.extend( PaginatorClientSideMixin, {
shoesFilteredOption1: function() {
return this.get('arrangedContent') // 'arrangedContent' is the sorted list of underlying content; assumes your backing model is the DS.RecordArray of shoes
// this use of slice takes an array and returns the first 10 elements
.slice( 0, 10 );
// we depend on 'arrangedContent' because everytime this changes, we need to recompute this value
}.property('arrangedContent')
shoesFilteredOption2: function() {
return this.get('arrangedContent') // 'arrangedContent' is the sorted list of underlying content; assumes your backing model is the DS.RecordArray of shoes
// here we're filtering the array to only return "active" shoes
.filter( function(item, index, self ) {
if (item.isActive) { return true; }
})
}.property('arrangedContent')
});
Then on your Handlebars template read from shoesFilteredOption1 instead of content or model.
In the data-driven paradigm of Backbone, the backbone views/routers should subscribe to model changes and act based on the model change events. Following this principle, the application's views/routers can be isolated from each other, which is great.
However, there are a lot of changes in the states of the applications that are not persisted in the models. For example, in a to-do app, there could be buttons that lets you look at tasks that are "completed", "not completed", or "all". This is an application state not persisted in the model. Note that the completion state of any task is persisted, but the current filter in the view is a transient state.
What is a good way to deal with such application state? Using a plain, non-backboned state means that the views/routers cannot listen to the changes in this state, and hence become difficult to code in the data-driven paradigm.
Your buttons filter example can be properly solved using Model events.
I suppose your buttons handlers have access to the tasks Collection. Then filter the collection and trigger events over the selected Models like:
model.trigger( "filter:selected" )
or
model.trigger( "filter:un-selected" )
The ModelView can be listening to these events on its Model and acts accordingly.
This is following your requirements of respecting the not use or "attributes that are not persistent" like selected but I don't have any trauma to use special attributes even if they shouldn't be persistent. So I also suggest to modify the selected attribute of your Models to represent volatile states.
So in your buttons handlers filter the collection and modify the selected attribute in your Models is my preferred solution:
model.set( "selected", true )
You can always override Model.toJSON() to clean up before sync or just leave this special attributes to travel to your server and being ignored there.
Comment got long so I'll produce a second answer to compare.
First, I feel like "completed", "not completed" are totally item model attributes that would be persisted. Or maybe if your items are owned by many users, each with their own "completed" "not completed" states, then the item would have a completedState submodel or something. Point being, while #fguillen produced two possible solutions for you, I also prefer to do it his second way, having models contain the attributes and the button / view doing most of the work.
To me it doesn't make sense for the model to have its own custom event for this. Sounds to me like a filter button would only have to deal with providing the appropriate views. (Which items to show) Thus, I would just make the button element call a function that runs a filter on the collection more or less directly.
event: {
'click filterBtnCompleted':'showCompleted'
},
showCompleted: function(event) {
var completedAry = this.itemCollection.filter(function(item) {
return item.get('completed');
});
// Code empties your current item views and re-renders them with just filtered models
}
I tend to tuck away these kind of convenience filter functions within the collection themselves so I can just call:
this.ItemCollection.getCompleted(); // etc.
Leaving these attributes in your model and ignoring them on your server is fine. Although again, it does sound to me like they would be attributes you want to persist.
One more thing, you said that using plain non-backboned states sacrifices events. (Grin :-) Not so! You can easily extend any object to have Backbone.Event capabilities.
var flamingo = {};
_.extend(flamingo, Backbone.Events);
Now you can have flamingo trigger and listen for events like anything else!
EDIT: to address Router > View data dealings -------------------//
What I do with my router might not be what you do, but I pass my appView into the router as an options. I have a function in appView called showView() that creates subviews. Thus my router has access to the views I'm dealing with pretty much directly.
// Router
initialize: function(options) {
this.appView = options.appView;
}
In our case, it may be the itemsView that will need to be filtered to present the completed items. Note: I also have a showView() function that manages subviews. You might just be working directly with appView in your scenario.
So when a route like /items/#completed is called, I might do something like this.
routes: {
'completed':'completed'
},
completed: {
var itemsView = ItemCollectionView.create({
'collection': // Your collection however you do it
});
this.appView.showView(itemsView);
itemsView.showCompleted(); // Calls the showCompleted() from View example way above
}
Does this help?
I'm trying to render an item at the start of a collection (imagine if you were posting a new record on facebook)
When I come to add(response, {at: 0}); into the collection, the record is correctly inserted at 0 into the collection, but is rendered at the bottom of the list of items. I'm confused as I had this working before, but I think what I was doing in a hacky style, was just reset and re-rendering the collection.
I'm wondering what the tidy way to handle this, and where should I bind the logic.
Is it on the add method of the collection? Currently this is empty (but I am using Marionette) and I feel that this overrides the default rendering of backbone. How do I take control of it again, so I can correctly get my new item to be added to the list, without destroying it all and re-creating it.
In Marionette, the default way to add a new item to a collection in the views, is to use jQuery's append method. The CollectionView type has a method called appendHtml which is used to do the actual appending. (see http://derickbailey.github.com/backbone.marionette/docs/backbone.marionette.html#section-24 )
You can easily override this method in your specific collection view, though, and have the new model appended wherever it needs to go.
In your case, if you are always wanting to prepend the new model to the top of the list, it's very trivial to change your collection view to do this:
Backbone.Marionette.CollectionView.extend({
appendHtml: function(cv, iv){
cv.$el.prepend(iv.el);
}
});
Note that cv is the collection view instance and iv is the item view instance for the model in the collection.
If you need to do more complicated things like find an exact position in the existing collection of HTML nodes, you can do that within the appendHtml function as well. Of course this gets more complicated than just doing a prepend instead of an append, but it's still possible.
Hope that helps.
This might be a very old question, but I'll post my solution anyway...
It prepends/appends the itemViewContainer with new models depending on how they were added to the collection (unshift/add), by overriding the appendHtml function.
appendHtml: function(collectionView, itemView, index){
if(index > 0) {
collectionView.$el.find(this.itemViewContainer).append(itemView.el);
} else {
collectionView.$el.find(this.itemViewContainer).prepend(itemView.el);
}
},
I have a collection of models in my Backbone.js application.
It's a list of items that you can hover over with the mouse or navigate around with the keyboard.
If the mouse is hovering, or if the keyboard navigation has the item selected they will both do the same thing: set that particular item/model to be 'selected'.
So in my model, I have an attribute basically called
selected: false
When it's hovered over, or selected with the keyboard, this will then be
selected: true
But, what is the best way to ensure that when this one model is true, the others are all false?
I'm current doing a basic thing of cycling through each model in the collection and then set the selected model to be true. But I wonder if there is a better, more efficient way of doing this?
Being selected seems a responsibility outside of a model's scope. The notion of selected implies that there are others, but a model can only worry about itself. As such, I'd consider moving this responsibility elsewhere where the notion of many models and having one selected seems more natural and expected.
So consider placing this responsibility either on the collection as an association. So, this.selected would point to the selected model. And then you can add a method to return the selected model in a collection.
Alternatively, you could give this responsibility to the view. You might do this if the selectedness of a model is purely a concern in the View layer.
The by-product of removing the responsibility from the model is that you eliminate the need to cycle through the entire collection when a new model becomes selected.
This is the way I'm doing it. The collection stores a reference to the selected model, but the state is held in the model. This could then be adapted to allow more a more complex algorithm for choosing which models are selected.
JobSummary = Backbone.Model.extend({
setSelected:function() {
this.collection.setSelected(this);
}
});
JobSummaryList = Backbone.Collection.extend({
model: JobSummary,
initialize: function() {
this.selected = null;
},
setSelected: function(jobSummary) {
if (this.selected) {
this.selected.set({selected:false});
}
jobSummary.set({selected:true});
this.selected = jobSummary;
}
};
I did something like Mr Eisenhauer suggested. I wanted the concept of paged data as well. Didn't really want to mix in the selected, page number, etc into the collection itself, so I created a "model" for the table data and called it a Snapshot. Something like this:
JobSummary = Backbone.Model.extend({});
JobSummaryList = Backbone.Collection.extend({
model: JobSummary
};
JobSummarySnapshot = Backbone.Model.Extend({
defaults: {
pageNumber: 1,
totalPages: 1,
itemsPerPage: 20,
selectedItems: new JobSummaryList(), // for multiple selections
jobSummaryList: new JobSummaryList()
}
});
Check out Backbone.CollectionView, which includes support for selecting models when they are clicked out of the box. The hover case you could implement using the setSelectedModel method.
You might want to have a look at two components of mine:
Backbone.Select
Backbone.Cycle
Backbone.Select has a minimal surface area. As few methods as possible are added to your objects. The idea is that you should be able to use Backbone.Select mixins pretty much everywhere, with a near-zero risk of conflicts.
Backbone.Cycle is built on top of Backbone.Select. It adds navigation methods and a few more bells and whistles. It probably is the better choice for a greenfield project.