Several places in my Backbone application I'd like to have an instant search over a collection, but I'm having a hard time coming up with the best way to implement it.
Here's a quick implementation. http://jsfiddle.net/7YgeE/ Keep in mind my collection could contain upwards of 200 models.
var CollectionView = Backbone.View.extend({
template: $('#template').html(),
initialize: function() {
this.collection = new Backbone.Collection([
{ first: 'John', last: 'Doe' },
{ first: 'Mary', last: 'Jane' },
{ first: 'Billy', last: 'Bob' },
{ first: 'Dexter', last: 'Morgan' },
{ first: 'Walter', last: 'White' },
{ first: 'Billy', last: 'Bobby' }
]);
this.collection.on('add', this.addOne, this);
this.render();
},
events: {
'keyup .search': 'search',
},
// Returns array subset of models that match search.
search: function(e) {
var search = this.$('.search').val().toLowerCase();
this.$('tbody').empty(); // is this creating ghost views?
_.each(this.collection.filter(function(model) {
return _.some(
model.values(),
function(value) {
return ~value.toLowerCase().indexOf(search);
});
}), $.proxy(this.addOne, this));
},
addOne: function(model) {
var view = new RowView({ model: model });
this.$('tbody').append(view.render().el);
},
render: function() {
$('#insert').replaceWith(this.$el.html(this.template));
this.collection.each(this.addOne, this);
}
});
And a tiny view for each model...
var RowView = Backbone.View.extend({
tagName: 'tr',
events: {
'click': 'click'
},
click: function () {
// Set element to active
this.$el.addClass('selected').siblings().removeClass('selected');
// Some detail view will listen for this.
App.trigger('model:view', this.model);
},
render: function() {
this.$el.html('<td>' + this.model.get('first') + '</td><td>' + this.model.get('last') + '</td>');
return this;
}
});
new CollectionView;
Question 1
On every keydown, I filter the collection, empty the tbody
, and render the results, thereby creating a new view for every model. I've just created ghost views, yes? Would it be best to properly destroy each view? Or should I attempt to manage my RowView
s... creating each one only once, and looping through them to only render the results? An array in my CollectionView
perhaps? After emptying the tbody
, would the RowViews
still have their el
or is that now null and need to be re-rendered?
Question 2, Model Selection
You'll notice I'm triggering a custom event in my RowView
. I'd like to have a detail view somewhere to handle that event and display the entirety of my model. When I search my list, if my selected model remains in the search results, I want to keep that state and let it remain in my detail view. Once it is no longer in my results, I'll empty the detail view. So I'll certainly need to manage an array of views, right? I've considered a doubly linked structure where each view points to it's model, and each model to it's view... but if I'm to implement a singleton factory on my models in the future, I can't impose that on the model. :/
So what's the best way to manage these views?
The Collection associated with your CollectionView must be consistent with what you are rendering, or you'll run into problems. You should not have to empty your tbody manually. You should update the collection, and listen to events emitted by the collection in the CollectionView and use that to update the view. In your search method, you should only update your Collection and not your CollectionView. This is one way you can implement it in the CollectionView initialize method:
And in your search method, you can just reset your collection and the view will render automatically:
where
filteredModels
is an array of the models that match the search query. Note that once you reset your collection with filtered models, you'll lose access to the other models that were originally there before the search. You should have a reference to a master collection that contains all of your models regardless of the search. This "master collection" is not associated with your view per se, but you could use the filter on this master collection and update the view's collection with the filtered models.As for your second question, you should not have a reference to the view from the model. The model should be completely independent from the View - only the view should reference the model.
Your
addOne
method could be refactored like this for better performance (always use $el to attach subviews):I got a little bit carried away while playing with your question.
First, I would create a dedicated collection to hold the filtered models and a "state model" to handle the search. For example,
which would be instantiated as
Then I would create separated views for the list and the input fields: easier to maintain and to move around
BaseView
allows to change the DOM in place, see Backbone, not "this.el" wrapping for detailsThe instances would look like
And a demo of the search at this stage http://jsfiddle.net/XxRD7/2/
Finally, I would modify
CollectionView
to graft the row views in my render function, something likeAnother Fiddle http://jsfiddle.net/XxRD7/3/