Подтвердить что ты не робот

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

При каждом нажатии клавиши я фильтрую коллекцию, пустую tbody и выдаю результаты, создавая тем самым новое представление для каждой модели. Я только что создал призрак, да? Было бы лучше правильно уничтожить каждую точку зрения? Или я должен попытаться управлять моими RowView s... создавая каждый из них только один раз и прокручивая их, чтобы отображать только результаты? Может быть, массив в моем CollectionView? После опорожнения tbody, у RowViews все еще есть их el или это теперь значение null и нужно повторно отобразить?

Вопрос 2, Выбор модели

Вы заметите, что я запускаю пользовательское событие в моем RowView. Я хотел бы иметь подробный вид где-нибудь, чтобы обработать это событие и отобразить всю мою модель. Когда я просматриваю свой список, если моя выбранная модель остается в результатах поиска, я хочу сохранить это состояние и оставить его в моем подробном представлении. Как только это больше не будет в моих результатах, я очищу подробный вид. Поэтому мне, безусловно, нужно будет управлять массивом взглядов, не так ли? Я рассмотрел структуру с двойной связью, в которой каждая точка зрения указывает на ее модель, и каждая модель для нее просматривает... но если я буду использовать singleton factory для своих моделей в будущем, я не могу навязывать это на модели.:/

Так какой лучший способ управлять этими представлениями?

4b9b3361

Ответ 1

Я немного увлекся игрой с твоим вопросом.

Сначала я создаю выделенную коллекцию для хранения фильтрованных моделей и "модели состояния" для обработки поиска. Например,

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 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 на месте, см. Магистраль, а не" this.el " обертка для деталей

Экземпляры будут выглядеть как

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/

Ответ 2

Коллекция, связанная с вашим CollectionView, должна соответствовать тому, что вы рендерите, или вы столкнетесь с проблемами. Вам не нужно будет опорожнять труп вручную. Вы должны обновить коллекцию и прослушать события, выпущенные в коллекции CollectionView, и использовать их для обновления представления. В вашем методе поиска вы должны обновлять свою коллекцию, а не свой CollectionView. Это один из способов его реализации в методе инициализации CollectionView:


initialize: function() {
  //...

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

И в вашем методе поиска вы можете просто reset свою коллекцию, и представление автоматически будет отображаться:


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

где filteredModels - массив моделей, соответствующих поисковому запросу. Обратите внимание, что после reset вашей коллекции с фильтрованными моделями вы потеряете доступ к другим моделям, которые первоначально были там до поиска. Вы должны иметь ссылку на коллекцию, содержащую все ваши модели, независимо от поиска. Эта "основная коллекция" не связана с вашим видом как таковой, но вы можете использовать фильтр в этой основной коллекции и обновлять коллекцию представлений с помощью фильтрованных моделей.

Что касается вашего второго вопроса, вы не должны иметь ссылку на представление из модели. Модель должна быть полностью независимой от представления - только представление должно ссылаться на модель.

Ваш метод addOne может быть реорганизован таким образом для повышения производительности (всегда используйте $el для подключения subviews):


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