4
votes

I'll start with a clear question and explain on after :
How to properly encapsulate a model, contained in a CompositeView, into an ItemView

My problem is simple, but I can't manage to get something good working. I have a tree of Notes. Each note is a Backbone.model and has a @descendants collection, I then have a Backbone.Collection to represent that tree (of which @descendants is an instance) I setup a basic CollectionView, with, as an itemView, the CompositeView. Everything rendered perfectly and I was quite happy about it. This working solution is shown in the first snippet of code!

Here comes the problem. Building on this, I kinda ran into this issue : Events fired for whole hierarchy which make sense for how my events where bound. At first, I simply added unique selectors (added data-guid to my html elements so I could get them by it) and it worked alright, although it was already getting "hacky". But other similar problem have risen so I decided I needed a solution for this. So, I told myslef, let's encapsulate each model in an ItemView and use the Composite view to recursively render them. But... This has not been for the best in all worlds...

Here is what I had in the beginning :

class Note.ModelView extends Marionette.CompositeView
  template: "note/noteModel"
  id: "note-item"
  itemViewContainer: ".note-descendants"
  ui:
    noteContent: ".noteContent"
  events:
    "keypress .noteContent": "createNote"
    "click .destroy": "deleteNote"
  # ...
  initialize: ->
    @collection = @model.descendants
  # ...

class Note.CollectionView extends Marionette.CollectionView
  id: "note-list"
  itemView: Note.ModelView
  initialize: ->
    @listenTo @collection, "sort", @render

Now I transferred every thing that belongs to rendering the Model in a new ItemView

  class Note.ModelView extends Marionette.ItemView
    template: "note/noteModel"

    ui:
      noteContent: ".noteContent"
    events: ->
      guid = @model.get 'guid'
      events = {}
      events["keypress #noteContent#{guid}"] = "createNote"
      events["blur #noteContent#{guid}"] = "updateNote"
      events["click #destroy#{guid}"] = @triggerEvent 'deleteNote'

    initialize: ->
      @bindKeyboardShortcuts()
      @listenTo @model, "change:created_at", @setCursor

  class Note.TreeView extends Marionette.CompositeView
    template: "note/parentNote"
    itemView: Note.ModelView

    initialize: ->
      @collection = @model.descendants
      @listenTo @collection, "sort", @render
      _.bindAll @, "renderItem"
    renderItem: (model) ->
      if model.get('parent_id') is 'root'
        itemView = new Note.ModelView model: model
        itemView.render()
        @$el.append itemView.el
    onRender: ->
      @collection.each this.renderItem
      @renderItem @model
    appendHtml: (collectionView, itemView) ->
      @model.descendants.each (note) =>
        iv = new Note.ModelView model: note
        iv.render()
        @.$('.note-descendants').append iv.el


  class Note.CollectionView extends Marionette.CollectionView
    id: "note-list"
    itemView: Note.TreeView
    initialize: ->
      @listenTo @collection, "sort", @render

and the noteMode template (parentNote is just an empty template right now)

<button class="btn btn-danger btn-xs untab">UN</button>
<button class="btn btn-success btn-xs tab">TAB</button>
<div class="noteContent">{{{indent}}}{{{title}}}</div>
<button class="destroy"></button>
<div class="note-descendants"></div> # That is where I'm putting the children notes

So this is almost working. But it's hacky and.. well it still doesn't quite work. I've been reading through all documentations, and I've read those two links Derick Bailey on nested structure and CompositeView and David Sulc on Nested Views but I still feel I'm missing some important details.

What I'm looking for, as an answer, is any hint or any common way to manage this with Marionette. I'm really looking for something clean since this is going to be one of the angular piece on which the app will be built.
Maybe also I'm searching in the wrong place? Anything will be welcome!! Thank you very much

I wish you all a very nice night. Thank you for your time.

1
Thank you for linking a link that is already in the question!..Mikechaos

1 Answers

3
votes

I'm going to explain to you, in different words, what Derick Bailey already stated in the blog post you referenced.

Views

You need two views to make this work.

The first, the "mother view", is the outer most view which contains the recursive structure as its child nodes. This may either be a Marionette.CollectionView or a Marionette.CompositeView.

The second view is your recursive view, meaning it needs to be a Marionette.CompositeView. You make a Marionette.CompositeView recursive, by NOT specifiying an "itemView" property. In lack of an itemView property, a Marionette.CompositeView will use ITSELF to render the models of the collection it was given.

Collection

You need a collection that is either a Backbone.Collection of nested Backbone.Collections, or simply a Backbone.Collection which contains a deeply nested hash. In the latter case, you need not forget to convert the simple hashes to Backbone.Collections when handing them to a Marionette.CompositeView (as done below in the RecursiveView's initialize() method).

Putting it all together

var RecursiveView = Marionette.CompositeView.extend({
    template: '#someTemplate',
    initialize: function () {
        this.collection = this.model.get('children')
    }
});

var MotherView = Marionette.CollectionView.extend({
    itemView: RecursiveView,
    collection: new Backbone.Collection([{key: 'value', children: [{}, {}, {}]}])
});

Problem: Event Bubbling

Naturally, DOM events will now bubble up, so this code

var RecursiveView = Marionette.CompositeView.extend({
    events: {
        'click button': someCallback
    }
});

will be a catch-all for the events of the view it references as well as all its descendants.

The easiest solution to this problem is the use of the direct descendant css selectr >, e.g.

var RecursiveView = Marionette.CompositeView.extend({
    events: {
        'click >button': someCallback
    }
});

If that isn't enough, one effective solution would be to prevent the automatic event bubbling, e.g.

var RecursiveView = Marionette.CompositeView.extend({
    events: {
        'click button': function (event) {
            event.stopPropagation();
            // or event.stopImmediatePropagation() depending on your use case
            // cf. http://stackoverflow.com/a/5299841/899586 and 
        }
    }
});