Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I render DOM-dependent views in a CollectionView?

I have a Marionette.CollectionView which renders a list of ItemViews. During render(), I use the ItemView's model to draw some SVG using Raphael.

Raphael requires that I specify a height and a width for its canvas, which I would normally grab from this.$el. However, $el (as an empty <div>) has no dimensions until it is added to the DOM and CSS rules are applied to it, so I need to delay rendering until I know that the view is in the DOM.

The problem is that Marionette.CollectionView doesn't add a child view to the DOM until after it has rendered. How can I override this behavior without re-implementing half of CollectionView?

Sample code

// Renders a single object.
var ItemView = Marionette.ItemView.extend({
    template: "#item-view-template",
    onRender: function() {
        var svgEl = this.$el.find("div.svg-canvas");
        // Raphael needs the element's width and height, which
        // is 0 until this.$el is in the DOM.
        var paper = Raphael(svgEl.get(0), svgEl.height(), svgEl.width());
        // ... draw some SVG...
    }
});

// Renders a collection of objects.
var ListView = Marionette.CollectionView.extend({
    itemView: ItemView,
    model: MyModel
});

// Main point of entry.
MyApp.show = function() {
    var collection = new MyCollection();
    var listView = new ListView({ collection: collection });
    MyApp.mainRegion.show(listView);
    collection.fetch();
};
like image 637
Brant Bobby Avatar asked Jun 04 '12 19:06

Brant Bobby


1 Answers

onRender won't handle your needs, as this method gets called when the view has been rendered - but does not guarantee that the view has been added to the DOM yet.

To do that, you'll need an onShow method which will be called by the region when you show the view in the region. The problem is that the current implementation only calls onShow on the view you directly pass in - the collection view in this case. So you would need to implement your onShow in a way that makes it call that method on all of the collection views' children.


Marionette.CollectionView.extend({
  // ...  

  onShow: function(){
    _.each(this.children, function(childView){
      if (childView.onShow){ childView.onShow(); }
    });
  }
});

That should do it. When you call MyApp.mainRegion.show(listView) it will call the onShow method of the collection view, which will then call it on the children (if it's there).


Per discussion in the comments, an implementation that will guarantee the onShow method of the child views is called, even after the onShow of the parent view has been called and an item is added to the collection later:


ItemView = Marionette.ItemView.extend({
  // ...

  onShow: function(){
    // I am guaranteed to be called from the CollectionView
    // because the CollectionView registers me in a promise
  }
});

CollectionView = Marionette.CollectionView.extend({

  initialize: function(){
    this.onShowCallbacks = new Marionette.Callbacks();
  },

  onShow: function(){
    this.onShowCallbacks.run();
  },

  appendHtml: function(cv, iv){
    cv.append(iv.el);

    // promise to run 'onShow' if it exists
    if (iv.hasOwnProperty("onShow")){
      this.onShowCallbacks.add(iv.onShow);
    }
  }
});

Also available in this gist: https://gist.github.com/2872526

like image 101
Derick Bailey Avatar answered Nov 03 '22 00:11

Derick Bailey