2
votes

I'm working on creating a basic survey app to learn Marionette/get a good grasp on nested models (answers > questions > survey). I'm using backbone-relational to accomplish this.

In the survey view, I use a composite view to listen each question. Each question is also a composite view, that loops through each question's answers. This all works. I'm having trouble figuring out how to access/bubble up events that happen in the answer ItemViews and question CompositeView. Any idea how I should be doing this?

Typically when I'm just using one CompositeView I can use say this in the view:

triggers:
   "click a.delete": "delete:clicked"

And then in the controller

@listenTo view, "childview:delete:clicked", ->
  console.log "delete clicked"

But this pattern doesn't seem to work for my current structure. I also tried the route of triggering an event when the delete button is clicked > manually triggering the event, but this didn't work beyond logging to the console correctly.

Any ideas where I'm going wrong? Any help is very much appreciated!

Controller:

@example.module "SurveysApp.List", (List, App, Backbone, Marionette, $, _) ->

  class List.Controller extends App.Controllers.Application

    initialize: ->
      survey = App.request "survey:entity", 1

      App.execute "when:fetched", survey, =>
        @layout = @getLayoutView()

        @listenTo @layout, "show", =>
          @panelRegion()
          @questionRegion survey
          @bannerRegion survey

        @show @layout

    questionRegion: (survey) ->
      questions = survey.get('questions')
      questionView = @getQuestionView questions, survey

      @listenTo questionView, "answer:delete:clicked", (child, args) ->
        console.log "answer deleted!"
        # model = args.model 
        # if confirm "Are you sure you want to delete #{model.get("title")}?" then model.destroy() else false

      @listenTo questionView, "childview:question:delete:clicked", (child, args) ->
        console.log "question deleted!"
        # model = args.model 
        # if confirm "Are you sure you want to delete #{model.get("title")}?" then model.destroy() else false

      @show questionView, region: @layout.questionRegion

    panelRegion: ->
      panelView = @getPanelView()
      @show panelView, region: @layout.panelRegion

    bannerRegion: (survey) ->
      bannerView = @getBannerView survey
      @show bannerView, region: @layout.bannerRegion

    getLayoutView: ->
      new List.Layout

    getBannerView: (survey) ->
      new List.Banner
        model: survey

    getPanelView: ->
      new List.Panel

    getQuestionView: (questions, survey) ->
      new List.Questions
        collection: questions
        model: survey

View:

@example.module "SurveysApp.List", (List, App, Backbone, Marionette, $, _) ->

  class List.Layout extends App.Views.Layout
    template: "surveys/list/list_layout"

    regions: 
      questionRegion: "#question-region"
      newRegion: "#new-region"
      panelRegion: "#panel-region"
      bannerRegion: "#banner-region"

  class List.Answer extends App.Views.ItemView
    template: "surveys/list/_answer"
    className: "answer-container"

    events:
      "click a.delete-button": "deleteAnswer"

    deleteAnswer: (e) ->
      e.preventDefault()
      @trigger "answer:delete:clicked", @model
      console.log "deleteAnswer"

  class List.Question extends App.Views.CompositeView
    template: "surveys/list/_question"
    id: "1000"
    className: "step"
    initialize: ->
      @collection = @model.get("answers")

    itemView: List.Answer 
    itemViewContainer: ".answer-container"

    triggers:
      "click .delete-question-button": "quesiton:delete:clicked"

  class List.Questions extends App.Views.CompositeView
    template: "surveys/list/_questions"
    className: "questions"
    itemViewContainer: ".editor"
    itemView: List.Question 

  class List.Banner extends App.Views.ItemView
    template: "surveys/list/_banner"
    id: "survey-builder"
    className: "my-fluid-container"

  class List.Panel extends App.Views.ItemView
    template: "surveys/list/_panel"
    id: "add-question"

EDIT: Adding HTML

Layout View:

<div id="banner-region"></div>

<div class="container">
    <div id="question-region"></div>
    <div id="panel-region"></div>
    <div id="new-region"></div>
</div>

Banner View:

<div class="container">
    <a id="back" href="#">
      <i class="fa fa-chevron-left"></i></span>
    </a>
    <div class="message"><%= @title %></div>
    <div class="btn btn-inline pull-right checkout">Checkout</div>
    <div class="btn btn-inline pull-right">Preview</div>
    <div class="btn btn-inline pull-right">Save and Close</div>
</div>

Questions CompositeView:

<div class="editor ui-sortable">

</div>

Question CompositeView:

<div class="row">
  <div class="col-sm-offset-1 col-sm-1">
    <div class="step-number">1</div>
  </div>
  <div class="col-sm-8">
    <div class="question-editor" id="editor-1000" data-question-id="1000">
      <div class="question-heading">
        <div  id="type" class="type">
          <div class="btn-group">
            <button type="button" id="question-select" class="btn dropdown-toggle" data-toggle="dropdown">
            <% if @single_response: %>
            Single Select 
            <% else: %>
            Multiple Select 
            <% end %>
            <span class="caret"></span></button>
            <ul class="dropdown-menu" role="menu">
              <li>
                <a href="#" class="single-select">Single Select</a>
              </li>
              <li>
                <a href="#" class="multi-select">Multiple Select</a>
              </li>
            </ul>
          </div>
          <div class="btn-group">
            <button type="button" id="question-select"><i class="fa fa-cog"></i>
          </div>
        </div>
        <div class="controls">
          <div class="btn delete-question-button">
            <i class="fa fa-times"></i>
          </div>
        </div>
      </div>
      <div class="question-title">
        <input id="question-title" type="text" placeholder="Question text" value="<%= @title %>">
      </div>
      <div class="answers shadowed-scrollbox">
        <div class="answer-container"></div>
        <div class="btn add-answer">
          <i class="fa fa-plus-square"> Add Answer</i>
        </div>
      </div>
      <div class="answer-options">
        <input type="checkbox" id="randomize-1000" class="randomize-answers">
        <label for="randomize-1000">Randomize answer order
        <br>
      </div>
    </div>
  </div>
</div>

Answer View:

<div class="row">
  <div class="col-sm-9">
    <input id="answer-title" type="text" placeholder="Answer text" class="answer" value="<%= @title %>">
  </div>
  <div class="col-sm-3">
    <div id="answer-buttons" class="btn-group">
      <a class="btn manage-skip"><i class="fa fa-code-fork"></i></a>
      <a class="btn btn-danger delete-button"><i class="fa fa-times"></i></a>
    </div>
  </div>
</div>

Panel View:

<i class="fa fa-plus-square"> Add Question</i>

EDIT

I'd also note that I can put the delete logic directly in the view, so this allows me to actually delete the answer. Buut it seems like this logic really belongs in a controller via bubbling up an event.

class List.Answer extends App.Views.ItemView
    template: "surveys/list/_answer"
    className: "answer-container"

    events:
      "click a.delete-button": "deleteAnswer"

    deleteAnswer: (e) ->
      e.preventDefault()
      if confirm "Are you sure you want to delete #{@model.get("title")}?" then @model.destroy() else false
      @trigger "answer:delete:clicked", @model
      console.log "deleteAnswer"
1
Could you post your HTML excerpt?P. R. Ribeiro
Sure thing - Just updated the post with the different view HTML.Tom Hammond
Basically the layout creates the initial questions CompositeView, which has a childView of the question CompositeView, which in addition to posting the question info also lists each Answer childView using the answer collection each question has.Tom Hammond

1 Answers

3
votes

I figured it out. You have to listen for the childview events within the nested composite's view initialize method and then bubble that up to the controller.

So I can trigger then event > then retrigger it again up to the controller:

class List.Answer extends App.Views.ItemView
    template: "surveys/list/_answer"
    className: "answer-container"

    triggers:
      "click a.delete-button": "answer:delete:clicked"

  class List.Question extends App.Views.CompositeView
    template: "surveys/list/_question"
    id: "1000"
    className: "step"
    initialize: ->
      @collection = @model.get("answers")
      @model.set(question_number: @model.collection.indexOf(@model) + 1)
      @on "childview:answer:delete:clicked", (child, args) =>
        args.collection = @model.get('answers')
        @trigger "answer:delete:clicked", args

And listen and act on it:

questionRegion: (survey) ->
      questions = survey.get('questions')
      questionView = @getQuestionView questions, survey

      @listenTo questionView, "childview:answer:delete:clicked", (child, args, answers) ->
        model = args.model
        answers = args.collection
        answers.remove(model)
        model.destroy()