CollectionView

Backbone Collections are composed of many models, so when rendering a collection what we need is to render a list of Views:

class CollectionView extends Backbone.View {
  render() {
    // Render a view for each model in the collection
    var html = this.collection.map(model => {
      var view = new this.modelView(model);
      view.render();
      return view.$el;
    });

    // Put the rendered items in the DOM
    this.$el.html(html);
    return this;
  }
}

Note that the modelView property should be a View class; it could be our ModelView class of the previous section or any other view. See how for each model in the collection it instantiates and renders a this.modelView with the current model. As a result, an html variable will contain an array of all rendered views. Finally the html array can be attached easily to the $el element.

For an example of how to use CollectionView, see the following example:

class MyModelView extends ModelView {
  // …
)

class MyView extends CollectionView {
  constructor(options) {
    super(options);
    this.el = '#main';
    this.modelView = MyModelView;
  }
}

var view = new MyView({collection: someCollection});
view.render();

This snippet will do the job, it will render a MyModelView for each model in the someCollection object and put the result list in the #main element.

However, if you add models to the collection or remove them, the view will not be updated. That's not a desirable behavior. When a model is added, it should add a new view at the end of the list; if a model is deleted from the collection, the view associated with that model should be deleted.

A quick and dirty way to sync collection changes and views is to re-render the entire view on every change in the collection, but this approach is very inefficient because client resources are consumed when re-rendering views that don't need to change. A better approach should exist.

Adding new models

When a model is added to the collection an add event is triggered; we can create an event handler to update the view:

class CollectionView extends Backbone.View {
  initialize() {
    this.listenTo(this.collection, 'add', this.addModel);
  }

  // ...
}

When the addModel method is called, it should create and render a new view with the data of the model added and put it at the end of the list.

var CollectionView = Backbone.View.extend({
  // ...
  // Render a model when is added to the collection
  modelAdded(model) {
    var view = this.renderModel(model);
    this.$el.append(view.$el);
  }

  render() {
    // Render a view for each model in the collection
    var html = this.collection.map(model => {
      var view = this.renderModel(model);
      return view.$el;
    });

    // Put the rendered items in the DOM
    this.$el.html(html);
    return this;
  }

  renderModel(model) {
    // Create a new view instance, modelView should be
    // redefined as a subclass of Backbone.View
    var view = new this.modelView({model: model});

    // Keep track of which view belongs to a model
    this.children[model.cid] = view;

    // Re-trigger all events in the children views, so that
    // you can listen events of the children views from the
    // collection view
    this.listenTo(view, 'all', eventName => {
      this.trigger('item:' + eventName, view, model);
    });

    view.render();
    return view;
  }
}

A renderModel() method was added since both methods, render() and modelAdded(), need to render the model in the same way. The DRY principle was applied.

When a child view is rendered, it is useful to listen for all the events for the given view, so that we can listen for child events from the collection.

var myCollectionView = new CollectionView({...});

myCollectionView.on('item:does:something', (view, model) => {
  // Do something with the model or the view
});

Our event handler is very simple; it renders the added model with the renderModel() method, attaches an event handler for any event in the view, and appends the result at the end of the DOM element.

Deleting models

When a model is removed from the collection, the view that contains that model should be deleted from the DOM to reflect the current state of the collection. Consider an event handler for the removed event:

function modelRemoved(model) {
  var view = getViewForModel(model); // Find view for this model
  view.destroy();
}

How can we obtain the view associated with the model? There is no easy way to do it with the code that we have. To make it easy, we can keep track of model-view associations; in this way, getting the view is very easy:

class CollectionView extends Backbone.View {
  initialize() {
    this.children = {};
    this.listenTo(this.collection, 'add', this.modelAdded);
    this.listenTo(this.collection, 'remove', this.modelRemoved);
  }

  // ...

  // Close view of model when is removed from the collection
  modelRemoved(model) {
    var view = this.children[model.cid];

    if (view) {
      view.remove();
      this.children[model.cid] = undefined;
    }
  }

  // ...

  renderModel(model) {
    // Create a new view instance, modelView should be
    // redefined as a subclass of Backbone.View
    var view = new this.modelView({model: model});

    // Keep track of which view belongs to a model
    this.children[model.cid] = view;

    // Re-trigger all events in the children views, so that
    // you can listen events of the children views from the
    // collection view
    this.listenTo(view, 'all', eventName => {
      this.trigger('item:' + eventName, view, model);
    });

    view.render();
    return view;
  }
}

At rendering time, we store a reference to the view in the this.children hash table for future reference, since render() and modelAdded() use the same method to render; this change is done in one place, the renderModel() method.

When a model is removed, the modelRemoved() method can easily find the view and remove it by calling the standard remove() method and destroying the reference in the this.children hash.

Destroying views

When a CollectionView is destroyed, it should remove all children views to clean the memory properly. This should be done by extending the remove() method:

class CollectionView extends Backbone.View {
  // ...
 
  // Close view of model when is removed from the collection
  modelRemoved(model) {
    if (!model) return;

    var view = this.children[model.cid];
    this.closeChildView(view);
  }

  // ...

  // Called to close the collection view, should close
  // itself and all the live childrens
  remove() {
    Backbone.View.prototype.remove.call(this);
    this.closeChildren();
  }

  // Close all the live childrens
  closeChildren() {
    var children = this.children || {};

    // Use the arrow function to bind correctly the "this" object
    _.each(children, child => this.closeChildView(child));
  }

  closeChildView(view) {
    // Ignore if view is not valid
    if (!view) return;

    // Call the remove function only if available
    if (_.isFunction(view.remove)) {
      view.remove();
    }

    // Remove event handlers for the view
    this.stopListening(view);

    // Stop tracking the model-view relationship for the
    // closed view
    if (view.model) {
      this.children[view.model.cid] = undefined;
    }
  }
}

Now, when the view needs to be removed, it will do it and clean all the children views.

Resetting the collection

When a collection is wiped, the view should re-render the entire collection, because all items were replaced:

class CollectionView extends Backbone.View {
  initialize() {
    // ...
    this.listenTo(this.collection, 'reset', this.render);
  }

  // ...
}

This works, but previous views should be closed too; as we saw in the previous section, the best place to do it is in the render method:

class CollectionView extends Backbone.View.extend({
  // ...
  render () {
    // Clean up any previous elements rendered
    this.closeChildren();

    // Render a view for each model in the collection
    var html = this.collection.map(model => {
      var view = this.renderModel(model);
      return view.$el;
    });

    // Put the rendered items in the DOM
    this.$el.html(html);
    return this;
  }

  // ...
}

If a view has no items yet, the closeChildren() method will not do anything.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.72.212