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.
Related
What is the best practice for immediately persisting a model, when one of it's attributes is bound to a template input? Does it belong to the model or the controller, in your opinion?
I came up with this solution, based on an observer:
# Models
App.Foo = DS.Model.extend
bars: DS.hasMany('bars')
App.Bar = DS.Model.extend
quantity: DS.attr('number')
# Template
{{#each bar in bars}}
{{input value=bar.quantity}}
{{/each}}
# Controller
persistQuantity: ( ->
#get('bars').forEach (bar) -> bar.save() if bar.get('isDirty')
).observes('bars.#each.quantity')
But this fires multiple (3 for me) save requests for the same model for some reason.
I also tried to put the observer on the model, but this went to an endless loop:
# App.Bar Model
persistQuantity: ( ->
#save()
).observes('quantity')
I tried to fix that through Ember.run.once, but my understanding of the Ember run loop wasn't deep enough, apparently.
Where it belongs depends on whether or not you want the model to save whenever it changes, or only save when it changes from a particular view. If you want the model to always save, regardless of where it's saved, do it on the model. If you want to control saving it from a particular view do it in the controller.
Debouncing would be my favorite option for solving the multiple call issue. Watching a particular item, then automatically saving when it changes. You could also watch isDirty and fire when it changes, but I'm more a fan of the other pattern (though isDirty scales better, it's less informative). Here's both patterns, feel free to mix and match as appropriate.
Have the model auto save when dirty:
App.Bar = DS.Model.extend
quantity: DS.attr('number'),
watchDirty: function(){
if(this.get('isDirty')){
this.save();
}
}.observes('isDirty')
Example: http://emberjs.jsbin.com/OxIDiVU/898/edit
Have the model queue saving when an item gets dirty (or multiple items)
App.Bar = DS.Model.extend({
quantity: DS.attr(),
watchQuantity: function(){
if(this.get('isDirty')){
Em.run.debounce(this, this.save, 500);
}
}.observes('quantity')
});
Example: http://emberjs.jsbin.com/OxIDiVU/897/edit
I have a model for my sources and a model for each segment. Each source has many segments. Each segment has an action that toggles an isSelected property.
I need to keep an updated list of selected segments. My first thought was to do something like...
App.Source = Ember.Object.extend({
selectedSourceList = Em.A(),
selectedSourcesObserver: function() {
// code to update array selectedSourceList
}.observes('segment.isSelected')
});
.. but that observes() function isn't right. I'm new to ember, so my approach may be completely wrong.
How can a method in one model observe the properties of many other models?
EDIT: corrected names to indicate that the segment model is for a single segment - not a collection of segments (that is what I plan to do in the sources model).
I think there are three parts to your question:
how to observe a collection
observers in general
managing relationships
observing a collection
The #each property helps observe properties for items in a collection: segments.#each.isSelected
observers in general
.observes() on a function is a shorthand to set up an observer function. If your goal for this function is to update a collection you might be better served by using .property() which sets up an observer and treats the function like a property:
selectedSegments: function() {
return this.get('segments').filterProperty('isSelected', true);
}.property('segments.#each.isSelected')
This means selectedSegments is the subset of segments from this object that are selected and is automatically managed as items are dropped or marked selected.
managing relationships
For plain Ember Objects you will need to manage the relationships, pushing new items into the array, etc.
segments = Em.A(), //somehow you are managing this collection, pushing segments into it
Also note the difference between Ember Objects and Ember Models. Ember Data is an optional additional library that allows specifying models and relationships and helps to manage the models for you. With Ember data you might have something like:
App.Source = DS.Model.extend({
//ember-data helps manage this relationship
// the 'segment' parameter refers to App.Segment
segments: DS.hasMany('segments'),
selectedSegments: function() {
return this.get('segments').filterProperty('isSelected', true);
}.property('segments.#each.isSelected')
});
And App.Semgent
App.Segment = DS.Model.extend({
selection: DS.belongsTo('selection')
});
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 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.