Backbone.js - Best Practice for Implementing “Instant” Search

后端 未结 2 1517
隐瞒了意图╮
隐瞒了意图╮ 2021-01-31 06:14

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.

相关标签:
2条回答
  • 2021-01-31 06:33

    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/

    0 讨论(0)
  • 2021-01-31 06:34

    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);
    
    0 讨论(0)
提交回复
热议问题