Backbone.js - Best Practice for Implementing “Inst

2019-03-08 16:10发布

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.

Here's a quick implementation. http://jsfiddle.net/7YgeE/ Keep in mind my collection could contain upwards of 200 models.

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

And a tiny view for each model...

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;

Question 1

On every keydown, I filter the collection, empty the tbody, and render the results, thereby creating a new view for every model. I've just created ghost views, yes? Would it be best to properly destroy each view? Or should I attempt to manage my RowViews... creating each one only once, and looping through them to only render the results? An array in my CollectionView perhaps? After emptying the tbody, would the RowViews still have their el or is that now null and need to be re-rendered?

Question 2, Model Selection

You'll notice I'm triggering a custom event in my RowView. I'd like to have a detail view somewhere to handle that event and display the entirety of my model. When I search my list, if my selected model remains in the search results, I want to keep that state and let it remain in my detail view. Once it is no longer in my results, I'll empty the detail view. So I'll certainly need to manage an array of views, right? I've considered a doubly linked structure where each view points to it's model, and each model to it's view... but if I'm to implement a singleton factory on my models in the future, I can't impose that on the model. :/

So what's the best way to manage these views?

2条回答
Fickle 薄情
2楼-- · 2019-03-08 16:45

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);
查看更多
我命由我不由天
3楼-- · 2019-03-08 16:46

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/

查看更多
登录 后发表回答