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/