Backbone.js - 实施“即时”搜索的最佳实践

时间:2013-08-10 00:00:42

标签: javascript backbone.js

我的Backbone应用程序中的几个地方我想对一​​个集合进行即时搜索,但我很难找到实现它的最佳方法。

这是一个快速实施。 http://jsfiddle.net/7YgeE/请记住,我的收藏品可能包含超过200种型号。

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);
  }
});

每个模特的一个小视图......

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;

问题1

在每个keydown上,我过滤集合,清空tbody ,然后渲染结果,从而为每个模型创建一个新视图。我刚创造了鬼视图,是吗?是否最好正确销毁每个视图?或者我应该尝试管理我的RowView ...仅创建一个,并循环它们只渲染结果?我的CollectionView中的数组也许?清空tbody后,RowViews是否仍然拥有el或现在是否为空,需要重新呈现?

问题2,模型选择

您会注意到我在RowView中触发了自定义事件。我希望在某个地方有一个详细视图来处理该事件并显示我的整个模型。当我搜索我的列表时,如果我选择的模型保留在搜索结果中,我想保持该状态并让它保留在我的详细视图中。一旦它不在我的结果中,我将清空详细视图。所以我当然需要管理一系列观点,对吧?我考虑过一个双重链接的结构,每个视图都指向它的模型,每个模型都指向它的视图......但如果我将来在我的模型上实现一个单独的工厂,我不能强加于模型。 :/

那么管理这些观点的最佳方法是什么?

2 个答案:

答案 0 :(得分:20)

在玩你的问题时,我有点失望。

首先,我将创建一个专用集合来保存过滤后的模型和一个“状态模型”来处理搜索。例如,

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); 
    }
});

将被实例化为

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});

然后我会为列表和输入字段创建单独的视图:更容易维护和移动

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允许更改DOM,有关详细信息,请参阅Backbone, not "this.el" wrapping

实例看起来像

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);

此阶段的搜索演示http://jsfiddle.net/XxRD7/2/

最后,我会修改CollectionView以在我的渲染函数中移植行视图,例如

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;
    }
});

另一个小提琴http://jsfiddle.net/XxRD7/3/

答案 1 :(得分:4)

与您的CollectionView关联的集合必须与您呈现的内容一致,否则您将遇到问题。你不应该手动清空你的tbody。您应该更新集合,并在CollectionView中侦听集合发出的事件,并使用它来更新视图。在您的搜索方法中,您应该只更新您的Collection而不是CollectionView。这是您可以在CollectionView初始化方法中实现它的一种方法:


initialize: function() {
  //...

  this.listenTo(this.collection, "reset", this.render);
  this.listenTo(this.collection, "add", this.addOne);
}

在您的搜索方法中,您只需重置您的收藏,视图就会自动呈现:


search: function() {
  this.collection.reset(filteredModels);
}

其中filteredModels是与搜索查询匹配的模型数组。请注意,使用过滤后的模型重置集合后,您将无法访问搜索前最初存在的其他模型。您应该引用包含所有模型的主集合,而不管搜索是什么。此“主集合”本身与您的视图无关,但您可以在此主集合上使用过滤器,并使用过滤的模型更新视图的集合。

至于你的第二个问题,你不应该从模型中引用视图。模型应完全独立于视图 - 只有视图应引用模型。

您的addOne方法可以像这样重构以获得更好的效果(始终使用$ el来附加子视图):


var view = new RowView({ model: model });
this.$el.find('tbody').append(view.render().el);