3

I'm trying to construct a view in my app that will pop up polling questions in a modal dialog region. Maybe something like this for example:

    What is your favorite color?

      >Red
      >Blue
      >Green
      >Yellow
      >Other

    Submit Vote

I've read that Marionette js doesn't support forms out of the box and that you are advised to handle on your own.

That structure above, branch and leaves (question and list of options), suggests CompositeView to me. Is that correct?

How do I trigger a model.save() to record the selection? An html form wants an action. I'm unclear on how to connect the form action to model.save().

My rough draft ItemView and CompositeView code is below. Am I in the ballpark? How should it be adjusted?

var PollOptionItemView = Marionette.ItemView.extend({
    template: Handlebars.compile(
        '<input type="radio" name="group{{pollNum}}" value="{{option}}">{{option}}<br>'
    )
});


var PollOptionsListView = Marionette.CompositeView.extend({
    template: Handlebars.compile(

        //The question part
        '<div id="poll">' +
        '<div>{{question}}</div>' +
        '</div>' +

        //The list of options part
        '<form name="pollQuestion" action="? what goes here ?">' +
        '<div id="poll-options">' +
        '</div>' +
        '<input type="submit" value="Submit your vote">' +
        '</form>'
    ),

    itemView: PollOptionItemView,

    appendHtml: function (compositeView, itemView, index) {
        var childrenContainer = $(compositeView.$("#poll-options") || compositeView.el);
        var children = childrenContainer.children();
        if (children.size() === index) {
            childrenContainer.append(itemView.el);
        } else {
            childrenContainer.children().eq(index).before(itemView.el);
        }
    }
});

MORE DETAILS:

My goal really is to build poll questions dynamically, meaning the questions and options are not known at runtime but rather are queried from a SQL database thereafter. If you were looking at my app I'd launch a poll on your screen via SignalR. In essence I'm telling your browser "hey, go get the contents of poll question #1 from the database and display them". My thought was that CompositeViews are best suited for this because they are data driven. The questions and corresponding options could be stored models and collections the CompositeView template could render them dynamically on demand. I have most of this wired and it looks good. My only issue seems to be the notion of what kind of template to render. A form? Or should my template just plop some radio buttons on the screen with a submit button below it and I write some javascript to try to determine what selection the user made? I'd like not to use a form at all and just use the backbone framework to handle the submission. That seems clean to me but perhaps not possible or wise? Not sure yet.

Robert
  • 820
  • 2
  • 12
  • 26
  • You only need a compositeView if you're planning on using a backbone collection for the options. Regarding the form part, you can approach it like any other ajax form, e.g. http://stackoverflow.com/a/11855073/2347232. I'd listen to the click event on the form in my view (compositeView in this case) and use the approach shown in the example before. The key is to use `ev.preventDefault();` to prevent submitting the form so you can handle that yourself. If you'd like a fiddle example to elaborate, please say so. – Wilbert van de Ridder May 08 '14 at 06:51
  • Yes, I do plan to use collection for the options. I do this because the options will vary in number per poll question and I need a generic way to dynamically create the right number of radio buttons for each. I'm open to alternatives. On the form submission, seems odd to be doing ajax submission. Isn't backbone supposed to encapsulate ajax for such? The only reason I'm using a form at all is because I don't know how to make a radio button group otherwise. I really would kind of like to do this without a form. Just group some radio buttons together and handle a "Submit" click myself. – Robert May 08 '14 at 20:27
  • I'm looking at both options here... Need a minute to evaluate. My goal really is to dynamically build poll questions of unknown length and take a selection from the user for each. New polling questions of varying lengths can be added on the fly and have to be handled generically. The approach I outlined seemed like a good approach but I'm questioning it now. – Robert May 08 '14 at 20:29
  • Alright, that makes it a bit clearer. And where would you like to save the result of the poll, in a model I take it? Since you said the form will be built up dynamically, how do you plan to store it on the backend? – Wilbert van de Ridder May 08 '14 at 21:25
  • All good questions. Yes, would like to save results of the poll in a model. That model could then feed a "results" view. The poll questions and options are stored in a SQL database. Let me get out of this comments area and go back up and add more details to the main post. Maybe that will help. – Robert May 08 '14 at 22:51
  • Getting closer now :). In that case I'd suggest creating a model which can hold the question containing the type (multiple choice, open, etc.), the options (in case of for example multiple choice) and a result attribute. Then I think you can use a collectionView (or CompositeView for that matter) to render these models and select the appropriate template to use based on the type. I'll see if I can make a fiddle today. – Wilbert van de Ridder May 09 '14 at 07:26
  • Thank you for your thought suggestions here. I am studying them and will be trying. Back a bit later with a result. – Robert May 09 '14 at 14:17
  • One thing I'll note is that I'm using handlebars and not underscore as seen in my code above. I'd have to figure out the "each" part to render the options. There has to be some kind of for loop capability in handlebars. Reason I was thinking collection for the options was so that I wouldn't have to make special item views. Most questions will be multiple choice. Let me see if I can make it work in handlebars. – Robert May 09 '14 at 14:27
  • Added a handlebars example, see below. – Wilbert van de Ridder May 09 '14 at 15:12
  • See comment below accepted answer. Very elegant approach. Thanks much. – Robert May 09 '14 at 23:43

1 Answers1

4

I'd use the following approach:

  • Create a collection of your survey questions
  • Create special itemviews for each type of question
  • In your CompositeView, choose the model itemView based on its type
  • Use a simple validation to see if all questions have been answered
  • Output an array of all questions and their results.

For an example implementation, see this fiddle: http://jsfiddle.net/Cardiff/QRdhT/
Fullscreen: http://jsfiddle.net/Cardiff/QRdhT/embedded/result/

Note:

  • Try it without answering all questions to see the validation at work
  • Check your console on success to view the results

The code

// Define data
var surveyData = [{
    id: 1,
    type: 'multiplechoice',
    question: 'What color do you like?',
    options: ["Red", "Green", "Insanely blue", "Yellow?"],
    result: null,
    validationmsg: "Please choose a color."
}, {
    id: 2,
    type: 'openquestion',
    question: 'What food do you like?',
    options: null,
    result: null,
    validationmsg: "Please explain what food you like."
}, {
    id: 3,
    type: 'checkbox',
    question: 'What movie genres do you prefer?',
    options: ["Comedy", "Action", "Awesome", "Adventure", "1D"],
    result: null,
    validationmsg: "Please choose at least one movie genre."
}];

// Setup models
var questionModel = Backbone.Model.extend({
    defaults: {
        type: null,
        question: "",
        options: null,
        result: null,
        validationmsg: "Please fill in this question."
    },
    validate: function () {
        // Check if a result has been set, if not, invalidate
        if (!this.get('result')) {
            return false;
        }
        return true;
    }
});

// Setup collection
var surveyCollection = Backbone.Collection.extend({
    model: questionModel
});
var surveyCollectionInstance = new surveyCollection(surveyData);
console.log(surveyCollectionInstance);

// Define the ItemViews
/// Base itemView
var baseSurveyItemView = Marionette.ItemView.extend({
    ui: {
        warningmsg: '.warningmsg',
        panel: '.panel'
    },
    events: {
        'change': 'storeResult'
    },
    modelEvents: {
        'showInvalidMessage': 'showInvalidMessage',
        'hideInvalidMessage': 'hideInvalidMessage'
    },
    showInvalidMessage: function() {
        // Show message
        this.ui.warningmsg.show();

        // Add warning class
        this.ui.panel.addClass('panel-warningborder');
    },
    hideInvalidMessage: function() {
        // Hide message
        this.ui.warningmsg.hide();

        // Remove warning class
        this.ui.panel.removeClass('panel-warningborder');   
    }
});

/// Specific views
var multipleChoiceItemView = baseSurveyItemView.extend({
    template: "#view-multiplechoice",
    storeResult: function() {
        var value = this.$el.find("input[type='radio']:checked").val();
        this.model.set('result', value);
    }
});

var openQuestionItemView = baseSurveyItemView.extend({
    template: "#view-openquestion",
    storeResult: function() {
        var value = this.$el.find("textarea").val();
        this.model.set('result', value);
    }
});

var checkBoxItemView = baseSurveyItemView.extend({
    template: "#view-checkbox",
    storeResult: function() {
        var value = $("input[type='checkbox']:checked").map(function(){
            return $(this).val();
        }).get();
        this.model.set('result', (_.isEmpty(value)) ? null : value);
    }
});

// Define a CompositeView
var surveyCompositeView = Marionette.CompositeView.extend({
    template: "#survey",
    ui: {
        submitbutton: '.btn-primary'  
    },
    events: {
        'click @ui.submitbutton': 'submitSurvey'  
    },
    itemViewContainer: ".questions",
    itemViews: {
        multiplechoice: multipleChoiceItemView,
        openquestion: openQuestionItemView,
        checkbox: checkBoxItemView
    },
    getItemView: function (item) {
        // Get the view key for this item
        var viewId = item.get('type');

        // Get all defined views for this CompositeView
        var itemViewObject = Marionette.getOption(this, "itemViews");

        // Get correct view using given key
        var itemView = itemViewObject[viewId];


        if (!itemView) {
            throwError("An `itemView` must be specified", "NoItemViewError");
        }
        return itemView;
    },
    submitSurvey: function() {
        // Check if there are errors
        var hasErrors = false;
        _.each(this.collection.models, function(m) {
            // Validate model
            var modelValid = m.validate();

            // If it's invalid, trigger event on model
            if (!modelValid) {
                m.trigger('showInvalidMessage');   
                hasErrors = true;
            }
            else {
                m.trigger('hideInvalidMessage'); 
            }
        });

        // Check to see if it has errors, if so, raise message, otherwise output.
        if (hasErrors) {
            alert('You haven\'t answered all questions yet, please check.');   
        }
        else {
            // No errors, parse results and log to console
            var surveyResult = _.map(this.collection.models, function(m) {
                return {
                    id: m.get('id'),
                    result: m.get('result')
                }
            });

            // Log to console
            alert('Success! Check your console for the results');
            console.log(surveyResult);

            // Close the survey view
            rm.get('container').close();
        }
    }
});

// Create a region
var rm = new Marionette.RegionManager();
rm.addRegion("container", "#container");

// Create instance of composite view
var movieCompViewInstance = new surveyCompositeView({
    collection: surveyCollectionInstance
});

// Show the survey
rm.get('container').show(movieCompViewInstance);

Templates

<script type="text/html" id="survey">
    <div class="panel panel-primary"> 
        <div class="panel-heading"> 
            <h3 class="panel-title" > A cool survey regarding your life </h3>           
        </div>
        <div class="panel-body">
            <div class="questions"></div>
            <div class="submitbutton">
                <button type="button" class="btn btn-primary">Submit survey!</button>
            </div>
        </div>
    </div >
</script>

<script type="text/template" id="view-multiplechoice">
    <div class="panel panel-success"> 
        <div class="panel-heading"> 
            <h4 class="panel-title" > <%= question %> </h4>           
        </div>
        <div class="panel-body">
            <div class="warningmsg"><%= validationmsg %></div>
            <% _.each( options, function( option, index ){ %> 
                <div class="radio">
                  <label>
                    <input type="radio" name="optionsRadios" id="<%= index %>" value="<%= option %>"> <%= option %>
                  </label>
                </div>
            <% }); %>
        </div>
    </div>
</script>

<script type="text/template" id="view-openquestion">
<div class="panel panel-success"> 
        <div class="panel-heading"> 
            <h4 class="panel-title" > <%= question %> </h4>           
        </div>
        <div class="panel-body">
            <div class="warningmsg"><%= validationmsg %></div>
            <textarea class="form-control" rows="3"></textarea>
        </div>
    </div >
</script>

<script type="text/template" id="view-checkbox">
    <div class="panel panel-success"> 
        <div class="panel-heading"> 
            <h4 class="panel-title" > <%= question %> </h4>           
        </div>
        <div class="panel-body">
            <div class="warningmsg"><%= validationmsg %></div>
            <% _.each( options, function( option, index ){ %> 
                <div class="checkbox">
                  <label>
                    <input type="checkbox" value="<%= option %>"> <%= option %>
                  </label>
                </div>
            <% }); %>
        </div>
    </div>
</script>

<div id="container"></div>

Update: Added handlebars example
Jsfiddle using handlebars: http://jsfiddle.net/Cardiff/YrEP8/

  • Holy cow. That's a really thorough example and a very elegant approach - so much more so than the road I was going down. I'm still kind of studying things about it beyond my initial templating quandary. There are things in it that I'm not doing in my general Marionette development that I should be - the approach to validation, ItemView extensions, getItemView, ui hash, and the general organization. I'll be studying it carefully. Thanks much for this. – Robert May 09 '14 at 23:47
  • You're very much welcome. Good luck with your project! Keep in mind that also this is just one way to do it and improvements can be made. But at least it shows that it can be done. – Wilbert van de Ridder May 10 '14 at 07:12
  • Side question to which I've created another post (http://stackoverflow.com/questions/23599011/handlebars-each-block-helper-not-working-for-nested-object). If my options data were nested a level deeper in my JSON, is there an adjustment to the handlebars #each loop that might allow me to access them? – Robert May 12 '14 at 20:55
  • Nevermind. Please disregard last comment. Typo in my code. "#each options" works. Just misspelled :) Doh! – Robert May 13 '14 at 00:46