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.
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,
var Filter = Backbone.Model.extend({
defaults: {
what: '', // the textual search
where: 'all' // I added a scope to the search
},
initialize: function(opts) {
// the source collection
this.collection = opts.collection;
// the filtered models
this.filtered = new Backbone.Collection(opts.collection.models);
//listening to changes on the filter
this.on('change:what change:where', this.filter);
},
//recalculate the state of the filtered list
filter: function() {
var what = this.get('what').trim(),
where = this.get('where'),
lookin = (where==='all') ? ['first', 'last'] : where,
models;
if (what==='') {
models = this.collection.models;
} else {
models = this.collection.filter(function(model) {
return _.some(_.values(model.pick(lookin)), function(value) {
return ~value.toLowerCase().indexOf(what);
});
});
}
// let's reset the filtered collection with the appropriate models
this.filtered.reset(models);
}
});
which would be instantiated as
var people = 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'}
]);
var flt = new Filter({collection: people});
Then I would create separated views for the list and the input fields: easier to maintain and to move around
var BaseView = Backbone.View.extend({
render:function() {
var html, $oldel = this.$el, $newel;
html = this.html();
$newel=$(html);
this.setElement($newel);
$oldel.replaceWith($newel);
return this;
}
});
var CollectionView = BaseView.extend({
initialize: function(opts) {
// I like to pass the templates in the options
this.template = opts.template;
// listen to the filtered collection and rerender
this.listenTo(this.collection, 'reset', this.render);
},
html: function() {
return this.template({
models: this.collection.toJSON()
});
}
});
var FormView = Backbone.View.extend({
events: {
// throttled to limit the updates
'keyup input[name="what"]': _.throttle(function(e) {
this.model.set('what', e.currentTarget.value);
}, 200),
'click input[name="where"]': function(e) {
this.model.set('where', e.currentTarget.value);
}
}
});
BaseView
allows to change the DOM in place, see Backbone, not "this.el" wrapping for details
The instances would look like
var inputView = new FormView({
el: 'form',
model: flt
});
var listView = new CollectionView({
template: _.template($('#template-list').html()),
collection: flt.filtered
});
$('#content').append(listView.render().el);
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 like
var ItemView = BaseView.extend({
events: {
'click': function() {
console.log(this.model.get('first'));
}
}
});
var CollectionView = BaseView.extend({
initialize: function(opts) {
this.template = opts.template;
this.listenTo(this.collection, 'reset', this.render);
},
html: function() {
var models = this.collection.map(function (model) {
return _.extend(model.toJSON(), {
cid: model.cid
});
});
return this.template({models: models});
},
render: function() {
BaseView.prototype.render.call(this);
var coll = this.collection;
this.$('[data-cid]').each(function(ix, el) {
new ItemView({
el: el,
model: coll.get($(el).data('cid'))
});
});
return this;
}
});
Another Fiddle http://jsfiddle.net/XxRD7/3/
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:
initialize: function() {
//...
this.listenTo(this.collection, "reset", this.render);
this.listenTo(this.collection, "add", this.addOne);
}
And in your search method, you can just reset your collection and the view will render automatically:
search: function() {
this.collection.reset(filteredModels);
}
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):
var view = new RowView({ model: model });
this.$el.find('tbody').append(view.render().el);