Chapter 17: Taking and Reviewing Quizzes

In this final chapter, we’ll finish up our Quiz Engine application by implementing the Quiz module, which handles both taking and reviewing quizzes, depending on the state of the quiz.

Once again we’ll start by looking at the index.js file for the module, which will be in a new folder called Quiz inside the modules folder.

js/modules/Quiz/index.js

QuizEngine.module('Quiz', function(Quiz) {
    // Quiz Module Must be Manually Started
    Quiz.startWithParent = false;

    // Router needs to be created immediately, regardless of whether or not the module is started
    Quiz.controller = new Quiz.Controller();
    Quiz.router = new Quiz.Router({controller: Quiz.controller});

    Quiz.addInitializer(function(){
        Quiz.controller.show();
    });

    Quiz.addFinalizer(function(){
        Quiz.controller.hide();
        Quiz.stopListening();
    });

});

Looks familiar, doesn’t it? Yup, it’s the same as the other ones with the exception of the module name. Let’s move on to the next piece of the puzzle, then: the router.

js/modules/Quiz/Router.js

QuizEngine.module('Quiz', function(Quiz) {

    Quiz.Router = Marionette.AppRouter.extend({
        appRoutes: {
            "quiz/:cid": "showQuiz"
        }
    });

});

Once again, we have a single route. This time, though, we’re actually grabbing a parameter from the route: the cid of the quiz. Since there isn’t much to see here, let’s move on to the controller right away.

js/modules/Quiz/Controller.js

QuizEngine.module('Quiz', function(Quiz) {

    Quiz.Controller = Marionette.Controller.extend({
        // When the module starts, we need to make sure we have the correct view showing
        show: function() {
            // No Special Setup Needed: no-op
        },

        // When the module stops, we need to clean up
        hide: function() {
            QuizEngine.body.close();
            this.stopListening();
            this.data = this.view = null;
        },

        showQuiz: function(cid) {
            this._ensureSubAppIsRunning();
            this.stopListening();

            this.data = QuizEngine.module('Data').quizzes.get(cid);
            this.view = new Quiz.QuizView({model:this.data});

            QuizEngine.body.show(this.view);
            if (this.data) {
                if (this.data.isComplete()) {
                    this.showReview();
                }
                else {
                    this.listenTo(this.data, 'question:answered', this.showQuestion);
                    this.listenTo(this.data, 'completed', this.showReview);
                    this.showQuestion();
                }
            }
        },

        showReview: function() {
            var subView = new Quiz.QuizReviewView({model: this.data});

            this.renderSubView(subView);
        },

        showQuestion: function() {
            var question = this.data.getCurrentQuestion();

            if (question) {
                var subView = new Quiz.QuizQuestionView({
                    model: question, 
                    questionNumber: this.data.getCurrentPosition(),
                    quizLength: this.data.get('questions').length
                });

                this.renderSubView(subView);
            }
        },

        renderSubView: function(subView) {
            this.view.quizData.show(subView);
        },

        // Makes sure that this subapp is running so that we can perform everything we need to
        _ensureSubAppIsRunning: function() {
            QuizEngine.execute('subapp:start', 'Quiz');
        }
    });

});

Before I go into the controller’s details, I need to tell you how I decided to handle this module. First of all, we use the same route for taking the quiz and for reviewing it. The route simply says “show me the quiz” and depending on the state of the data, we could show a question that needs to be answered, or the review screen. Then, when a question is answered, the controller responds to the event by re-rendering with the next question. If the quiz is completed, it will instead show the review screen.

Also, we used a Layout here because whether we’re showing a single question or showing the full review, there is a part of the view that is constant: the quiz name. So we abstracted the quiz name into its own template, which is rendered by the Layout and then the rest of the view is separate and is shown via the Region on the Layout.

That explanation should help us understand what is going on as we traverse the methods of our controller. We start off with the familiar show and hide methods. I won’t go into them.

After that we come to showQuiz, which is the method that the router calls. As usual, we make sure that the current module is running. Since showQuiz is only called by the router, we have the controller stopListening to any events that it was listening to before, because it only listens to events on a single quiz. If someone went straight to one quiz after looking at a different quiz, then we need to stop listening to the events on the old quiz, otherwise there’s a possibility that we could react to changes on a quiz that we don’t currently care anything about.

After clearing the event listeners, we retrieve our quiz and create a QuizView, which is our layout. We immediately render it onscreen so that the regions are available. Also, if there was no quiz that matched the cid, the Layout will render a different template stating that the quiz doesn’t exist, so that’s another reason to have it rendered right away.

After that, we check to see if the quiz does indeed exist. If it does, we check to see if the quiz is completed. If it is, we render the review view; if not, we want to show the first available quiz question. Before we show the question, though, we register some event listeners on the quiz.

Every time a question is answered, we render a new question. This can be a problem after we answer the last question because there won’t be any more questions to render. That’s why we have a check in showQuestion to make sure there is a question available to render before it does anything.

Also, when a quiz is completed, we will show the review screen. In practice, when the final question is answered the 'question:answered' event will fire, which will call showQuestion, which will end up not showing anything. Then immediately afterward, the 'completed' event will fire and showReview will be called.

The showReview and showQuestion methods do pretty much what you’d expect, based on how view rendering was handled in the previous modules. There’s the obvious difference that these views will be rendered through a region on QuizView instead of through a region on QuizEngine. Also, QuizQuestionView requires some interesting information to be sent in since the model we give it is the question that it will render, and we’ll need some information from the quiz. Instead of giving both the quiz and the question to the view, we give it the individual pieces that it would need from the quiz. Either approach would work just fine.

Now let’s move on to the visual side of things.

We’ll start out by showing the templates and the class for QuizView. Add these templates into the index.html file:

index.html

<script type="text/template" id="quiz-quiz">
    <div class="col-xs-12">
        <h3><%= name %></h3>
        <div data-region="quizData"></div>
    </div>
</script>

<script type="text/template" id="quiz-quiz404">
    <div class="col-xs-12 text-center">
        <h3>This Quiz Does Not Exist</h2>
        <p><a href="#list">Return to Your Quizzes</a></p>
    </div>
</script>

The first template is what we’ll see most of the time. It shows the quiz name and it sets up an area for the region to connect. The second template is shown if the quiz doesn’t exist. It’s a simple message letting the user know that the quiz doesn’t exist. It then also provides a link back to the quiz list.

Now let’s look at the QuizView class itself:

js/modules/Quiz/views/QuizView.js

QuizEngine.module('Quiz', function(Quiz) {

    Quiz.QuizView = Marionette.Layout.extend({
        template: '#quiz-quiz',
        emptyTemplate: '#quiz-quiz404',

        getTemplate: function() {
            if (this.model) {
                return this.template;
            }
            else {
                return this.emptyTemplate;
            }
        },

        regions: {
            quizData: '[data-region=quizData]'
        }
    });

});

QuizView references two templates, then overrides the getTemplate method to retrieve the template we need, depending on whether we actually have a model or not. The only other thing we do is define our region. Simple enough.

Let’s move on to the view for quiz reviews.

Quiz Review Reminder
Quiz Review Reminder

1. This the quiz name, which is actually taken care of in the QuizView.

2. This is the first of the stats. We display the number of questions that were answered correctly. We can get this number from the getCorrect method on a quiz.

3. This is the total number of questions on the quiz. This is retrieved simply by getting the length of the questions collection on a quiz.

4. This is the score, which is available through the getScore method on a quiz.

5. Whether you use a checkmark or an “X” is determined by calling isCorrect on each individual question.

Here is the template for the review screen. As usual, add it to the index.html file.

index.html

<script type="text/template" id="quiz-quizreview">
    <div class="col-xs-12 col-sm-4">
        <div class="row">
            <h4 class="col-xs-4 col-sm-12 text-center">Correct: <%= getCorrect() %></h4>
            <h4 class="col-xs-4 col-sm-12 text-center">Total: <%= getTotal() %></h4>
            <h4 class="col-xs-4 col-sm-12 text-center">Score: <%= getScore() %></h4>
        </div>
    </div>
    <div class="col-xs-12 col-sm-8">
        <table class="table">
            <thead>
                <th class="col-xs-4">Question</th>
                <th class="col-xs-4">Correct</th>
            </thead>
            <tbody>
                <% _.each(questions, function(question, index) { %>
                    <tr>
                        <td><%= index + 1 %></td>
                        <td>
                            <% if (isCorrect(question.id)) { %>
                                <span class="glyphicon glyphicon-ok green"></span>
                            <% } else { %>
                                <span class="glyphicon glyphicon-remove red"></span>
                            <% } %>
                        </td>
                    </tr>
                <% }); %>
            </tbody>
        </table>
    </div>
</script>

You’ll notice that just about every bit of data we need for this template is retrieved through a template helper. The exceptions are the question number, which is determined by the index of the loop through the questions; and the question’s ID, which is used as the parameter of a template helper.

Inside this template you will notice that we’re looping through a collection again. This is partly because the collection is actually only a portion of the data, but also that it would be a waste of resources to create a view for each of these individual questions, especially since they have no functionality: they simply show the results.

This is actually true of the entire view. There are no events to listen for or react to. If it weren’t for all of the necessary template helpers, the view would be rather small and uninteresting.

js/modules/Quiz/views/QuizReviewView.js

QuizEngine.module('Quiz', function(Quiz) {

    Quiz.QuizReviewView = Marionette.ItemView.extend({
        template: '#quiz-quizreview',

        templateHelpers: function() {
            var quiz = this.model;

            return {
                getCorrect: function() {
                    return quiz.getCorrect();
                },
                getTotal: function() {
                    return quiz.get('questions').length;
                },
                getScore: function() {
                    return quiz.getScore() + "%";
                },
                isCorrect: function(qid) {
                    var question = quiz.get('questions').get(qid);

                    return question.isCorrect();
                }
            };
        }
    });

});

Like I said, there isn’t much here, except template helpers. For the most part, the template helpers are straightforward, but I’ll look at isCorrect in more detail. It seems a bit wasteful to grab the ID from a question just to pass that ID into a function that retrieves a question using that ID. The issue is that the question in the template is a JSON representation of the action question object, and therefore does not have the isCorrect method, which is what we need.

Alternatively, we could pass question into the template helper and change it to this:

isCorrect: function(question) {
    return question.chosenAnswer === question.question.correctAnswer;
}

We could even just use that conditional in the template instead of using a template helper. The issue with this solution is that I consider comparing chosenAnswer with question.correctAnswer to be an implementation detail, which should be hidden behind a method (just like it is in the real application). Even if you are OK with using this implementation detail instead of using the public API (the isCorrect method), you are duplicating code, which is obviously not a good thing.

Anyway, that was a long dissertation on a single, lowly template helper. Let’s leave isCorrect alone now and move on to taking a quiz.

Quiz Taking Reminder
Quiz Taking Reminder

1. Once again, we need to show the quiz name, which is handled by the Layout.

2. On this line, we have both the question number and the question text. The question number is one of the pieces of data that we passed in to the view from the controller.

3. We need to show some answer options to the user, otherwise it isn’t much of a quiz, is it? If the answer is left blank, then we’ll also need to inform the user that they made a mistake.

4. This button will simply fire off an event, which will then be used to change the chosenAnswer of the question. Then the question will throw around some events, which will lead to the quiz firing off events, which leads to the controller re-rendering with a new question. If we’re on the last question of the quiz, this button will say “Finish Quiz” instead of “Next Question”.

Here’s the template that makes all of this possible:

index.html

<script type="text/template" id="quiz-quizquestion">
    <p><strong><%= questionNumber() %>.</strong> <%= question.text %></p>
    <form id="form">
        <% _.each(question.answers, function(answer, index) { %>
            <div class="radio">
                <label>
                    <input type="radio" name="answers" value="<%= index %>">
                    <%= answer.text %>
                </label>
            </div>
        <% }); %>
        <button class="btn btn-primary" data-action="next"><%= isLastQuestion() ? "Finish Quiz" : "Next Question" %></button>
    </form>
</script>

Might as well show QuizQuestionView right away, too.

js/modules/Quiz/view/QuizQuestionView.js

QuizEngine.module('Quiz', function(Quiz) {

    Quiz.QuizQuestionView = Marionette.ItemView.extend({
        template: '#quiz-quizquestion',
        templateHelpers: function() {
            var options = this.options;

            return {
                questionNumber: function() {
                    return options.questionNumber;
                },
                isLastQuestion: function() {
                    return options.questionNumber === options.quizLength;
                }
            };
        },

        events: {
            'click [data-action=next]': 'submitAnswer'
        },

        submitAnswer: function(event) {
            var selectedAnswer = this.$(':radio:checked').val();

            if (_.isUndefined(selectedAnswer)) {
                alert("Please select an answer");
            }
            else {
                this.model.set('chosenAnswer', parseInt(selectedAnswer, 10));
            }

            event.preventDefault();
        }
    });

});

Now we can see how the template and view relate to each other more easily because they are right next to one another. The template uses the questionNumber helper, which simply returns the questionNumber option that was passed to the view’s constructor. Remember, the first parameter (which in most cases is an object literal) passed to a view’s constructor is stored on the view’s options property so they can all be accessed later.

The isLastQuestion helper uses both the questionNumber and the quizLength options that were passed in to determine if we are on the final question of the quiz, which is used to decide what the button says.

We also set up an event listener on the [data-action=next] button in order to submit the chosen answer. Once again, we do simple validation and use alert to alert users to an error. Then, if it passes, we just change the chosenAnswer of the question, firing off a series of events, as I’ve already mentioned.

Well, that’s everything. All we need to do is put the final scripts into the HTML, and we should have a fully functional Quiz Engine application. Place the scripts just above the js/initialize.js script file:

index.html

<script src="js/modules/Quiz/views/QuizView.js"></script>
<script src="js/modules/Quiz/views/QuizReviewView.js"></script>
<script src="js/modules/Quiz/views/QuizQuestionView.js"></script>
<script src="js/modules/Quiz/Controller.js"></script>
<script src="js/modules/Quiz/Router.js"></script>
<script src="js/modules/Quiz/index.js"></script>

Now fire it up!

More Complicated Applications

Sadly, this application doesn’t show you every type of scenario that you might run into when constructing a Marionette application. There are many larger applications with more complicated layouts and features that would require some more thought and organization than this. Here are a few different scenarios you might run into just with how you use modules:

Multiple modules needing to be run at the same time at different locations on the layout. Some of the modules may even be compatible to be used in multiple locations on the layout. Some changes and upgrades would be necessary for the SubAppManager at least.

Modules might need to be controlled by something other than routes. Some may be entirely reliant on the state of data to determine how and when they are used. Others may be controlled by events, requests, or commands. In these instances, you’ll likely set up the handlers and listeners in the module’s index.js file and have them call methods on the controller.

Some modules don’t have a life cycle: they are constantly on, as long as the application is. For example, if you have a module dedicated to navigation, it would likely persist throughout the session.

I wish there was a simple application that could fit into this book (this one barely does) that could show you everything you’ll ever need to know about all of Marionette’s capabilities, but it is quite likely that such an application does not exist, even outside the constraints of this book.

I can only hope that this Quiz Engine application has taught you everything you need to know right now to get started, and point you in the right direction of where to go in the other circumstances. I also hope you enjoyed learning about Marionette. This is where our journey together ends, so I wish you happy coding and God’s blessings.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset