Search code examples
javascriptdom-events

Dynamic Quiz using JavaScript - add a 'back' button


I am an amateur programmer and building a dynamic quiz as part of a beginner course on javascriptissexy's blog.

I have a bunch of questions stored in an array like this:

 var allQuestions = [
{question: "Which of the following is true about a cat's whiskers?", choices: ["Unlike fur, they don't shed at all", "Defend against predators", "They work as sensors", "They serve no purpose at all"], correctAnswer: 2},
{question: "How are cats different from dogs?", choices: ["Cats have the ability to move in the air", "Cats are highly social animals", "Dogs are independent but cats are the opposite", "Dogs love children but cats dont"], correctAnswer: 0},
{question: "What is a sure way of getting a cat's attention if you don't have treats?", choices: ["Call its name", "Stimulate the cat's curiousity", "Cats never pay attention to humans", "Serve some vegetarian food"], correctAnswer: 1},
];

Choices are set as radio input. Question and choices are displayed one at a time. When the user clicks next, the page displays next set of question and choices in the array. The page does not update if the user clicks next without making a selection - an alert pops up notifying the user to select an option. At the last question, the next button is changed to 'submit' and when the user clicks submit, the total score is displayed.

My problem is that I'm trying to add a 'previous' button so when clicked, the user can view the previous question (up to the first question) along with the selection they made and also be able to change their answer. I'm having a hard time defining the handler function and keeping track of the previous question's answer. Do I need to use a closure by any chance?

Here's a link to my code so far: fiddle


Solution

  • There are many approaches that would work here, but here's one way. First, track the answers the user submitted:

    function collectUserAnswer() {
        allQuestions[pos].userAnswer =
            document.querySelector('input[name="choice"]:checked').value; 
    }
    

    Then, as you go backwards, you will need to make sure you fill in the user's answer, if they selected one:

    function createChoices() {
        for(var i = 0; i < 4; i++) {
            var choice = document.getElementsByTagName("p")[i];
            var option = "<input id='ans_" + i 
                + "' type='radio' name='choice' value='" 
                + i + "'";
    
            if ( i == allQuestions[pos].userAnswer ) {
                option += " checked='checked'";   
            }
            option +=  ">" + allQuestions[pos].choices[i] 
                   + "</input>";
    
            choice.innerHTML = option;
        }
    }
    

    When you calculate the score, it will be easier to not have to keep track of a running total. With a running total, you have one set of logic that needs to happen when the user navigates forward and a different set of logic that happens when the user navigates backwards. Instead, we can use one set of logic that just tallies up the entire score every time:

    function calcScore() {
        var score = 0;
        for ( var i = 0; i < allQuestions.length; i++ ) {
            if ( allQuestions[i].userAnswer == allQuestions[i].correctAnswer ) {
                score++;
            }
        }
        this.totalScore = score;
    }
    

    Next, you'll have to duplicate the logic of creating the correct buttons depending on which question is currently selected, and put it in the updatePrevious() function. But whenever I see code duplication, I think that's a great opportunity to split the duplicate code into its own function so I can reuse it without duplicating it. That way, if you find a bug in that code, you don't have to find all the places you copied the bug into and fix it in all those places too. So, I would break it out like this:

    function updateNavigationButtons() {
        if(pos < len) {
            createQuestion();
            createChoices();
            submitButton.innerHTML = '<input id="next" type="button" name="nextbutton" value="Next Question" onclick="updateNext();"><input id="back" type="button" name="backbutton" value="Previous Question" onclick="updatePrevious();">';
        }
        if(pos == len-1) {
            submitButton.innerHTML = '<input id="submit" type="button" name="submitbutton" value="Submit Quiz" onclick="updateNext();"><input id="back" type="button" name="backbutton" value="Previous Question" onclick="updatePrevious();">';
        }
        if(pos == len) {
            while(firstDiv.hasChildNodes()) {
                firstDiv.removeChild(firstDiv.lastChild);
            }
            submitButton.innerHTML = "Your total score: " + this.totalScore; 
        }
    }
    

    Last, you'll need to make sure your previous and next button handlers call the new updateNavigationButtons() function:

    //update when user clicks next question
    function updateNext() {
        displayAlert();  
        collectUserAnswer();
        calcScore();
        pos++;
        updateNavigationButtons();
    };
    
    //update when user clicks previous question
    function updatePrevious() {
        displayAlert();  
        collectUserAnswer();
        calcScore();
        pos--;
        updateNavigationButtons();
    }
    

    There are still lots of ways I could improve this code. updateNext() and updatePrevious() both look almost identical except for what they do to the pos variable; so you might want to try to find a way to eliminate that code duplication. And before I get to that, I note that there is still a bug with navigating backwards -- an error happens if there is no answer selected.

    But hopefully this gets you a little closer. The fiddle for my changes can be found here.