Pergunta

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.

Foi útil?

Solução

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/

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top