You can see an index of all the posts in this series: go to index.
If you want to review my progress, here are the files from the end of the previous post: document object model
In the previous post, I identified that a problem with my quiz game is that, for the user to receive feedback on their answer, the page needs to be reset and the user will no longer be able to see their selection. I left myself with the challenge of displaying the user’s answer before telling them if they answered correctly or incorrectly.
Here’s a solution. First I need to update my index.html file to include each of the possible answers. I added the following code just below the form:
<p class="hidden" id="answer1">Your answer was Sydney</p> <p class="hidden" id="answer2">Your answer was Melbourne</p> <p class="hidden" id="answer3">Your answer was Canberra</p> <p class="hidden" id="answer4">Your answer was Brisbane</p>
Next. I changed the decisions in my JavaScript function to look like this:
if (answer == '3') { document.querySelector("#answer3").classList.remove('hidden'); correctFeedback.classList.remove('hidden'); nextQuestion.classList.remove('hidden'); } else if (answer != null) { incorrectFeedback.classList.remove('hidden'); if (answer == '1') { document.querySelector('#answer1').classList.remove('hidden'); } else if (answer == '2') { document.querySelector('#answer2').classList.remove('hidden'); } else if (answer == '4') { document.querySelector('#answer4').classList.remove('hidden'); } }
Notice that I’ve used a new way of showing those elements here. Instead of storing the output of document.querySelector in a variable, I’m using it directly to access the element’s classList. When I load the page in a browser I can see that it now reports my answer before giving me feedback.
It’s doesn’t seem like a very efficient way of doing things though. I’ve got to repeat all the answers in a series of hidden paragraphs and then I’ve got those really convoluted if … else if… else if… statements in the Javascript.
I gave myself another challenge which was to think about what problems I might face updating the second and third quiz pages to work in the same way. One thing I have noticed is that currently my JavaScript works only for the first question. It won’t work for the second or third questions because the correct answer is set to option 3!
Fortunately there’s a way to solve both these problems and it involves manipulating the DOM in a different way. This time, instead of just making existing HTML content visible, I’m going to create new content as well.
First, I changed the body of index.html as shown below:
<body> <h1>General Knowledge Quiz</h1> <p>Welcome to this fun quiz to test your general knowledge. See how easily you can answer the following questions.</p> <p class="question">What is the capital city of Australia?</p> <form action="index.html" method="GET"> <input type="radio" id="option1" name="answer" value="1"> <label id="label1" for="option1">Sydney</label><br> <input type="radio" id="option2" name="answer" value="2"> <label id="label2" for="option2">Melbourne</label><br> <input type="radio" id="option3" name="answer" value="3"> <label id="label3" for="option3">Canberra</label><br> <input type="radio" id="option4" name="answer" value="4"> <label id="label4" for="option4">Brisbane</label><br> <input type="submit"> </form> <p class="hidden" id="answerGiven"></p> <p class="hidden" id="correct"><strong>That's correct!</strong> Now try the next question...</p> <p class="hidden" id="incorrect"><strong>Sorry! That's not the right answer!</strong> Try again...</p> <p class="hidden" id="nextQuestion"><a href="question2.html">See question 2</a></p> <script> document.addEventListener('DOMContentLoaded', function() { checkAnswer('3'); }); </script> </body>
There are three changes here:
- my label elements all now have id attributes
- I’ve replaced all the hidden answer paragraph elements I had previously with a single answerGiven paragraph element in line 20 that has no content
- my event listener looks different.
One problem with using a function as an event listener in JavaScript, is that it’s not possible to pass an argument to that function. One way round this is to enclose the function call in something called an anonymous function. This is basically an unnamed function that gets registered as the event listener and which in turn calls the function I want, but with an argument.
I’ll be coming back to anonymous functions several times in future posts. For now, just know that this allows me to pass my checkAnswer function an argument, and that argument is a string containing the number of the correct answer option.
Now, I can change my JavaScript code to look like this:
function checkAnswer(correct) { let formValues = new URLSearchParams(window.location.search); let answer = formValues.get('answer'); let correctFeedback = document.querySelector('#correct'); let incorrectFeedback = document.querySelector('#incorrect'); let nextQuestion = document.querySelector('#nextQuestion'); let answerGiven = document.querySelector('#answerGiven'); let answerLabel = document.querySelector('#label' + answer); if (answer != null) { answerGiven.innerHTML = 'Your answer was ' + answerLabel.innerHTML; answerGiven.classList.remove('hidden'); if (answer == correct) { correctFeedback.classList.remove('hidden'); nextQuestion.classList.remove('hidden'); } else { incorrectFeedback.classList.remove('hidden'); } } }
You can see from line 1 that my function now begins with checkAnswer(correct). This means the function is now expecting to receive a single argument which will be stored in a variable called correct. So, when I call the function with checkAnswer(‘3’), which I do on line 26 of index.html, then correct is set to “3”. I use this on line 14 to check whether the user selected the correct answer.
You’ll notice, on line 7, that I get a reference to the empty answerGiven paragraph. On line 8 I’m using querySelector again, but this time I’m combining “#label” with the answer given by the user. This is another example of string concatenation. In this case it gives me a reference to the label for the selected answer.
Lines 11 to 19 are now enclosed in a single decision on line 10 to check that the user has actually answered the question.
If that is the case I can provide the relevant feedback. The bit of this that’s a new concept is line 11. I’m using a property of an element called innerHTML. A property is simply data associated with that element. This property holds the content of an element. So, for example, if I had a paragraph element that looked like this:
<p>This is a paragraph element.</p>
then the innerHTML property for this element would be “This is a paragraph element.”
So, when I use answerLabel.innerHTML I get the text for that answer option, e.g. “Sydney”. I then concatenate this with “Your answer was ” and I set the resulting string to be the innerHTML of my previously empty answerGiven paragraph.
When I reopen the page in a browser, I can see it now functions exactly as before. The big difference is that my code is now a lot more efficient and I can now reuse this JavaScript function in question3.html, by updating that file to look like this:
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>General Knowledge Quiz - Question 3</title> <link rel="stylesheet" href="css/quiz.css"> <script src="js/quiz.js"></script> </head> <body> <h1>General Knowledge Quiz</h1> <p class="question">What is the total area of the room shown in this plan?</p> <img src="images/html-question3-image.png"> <form action="question3.html" method="GET"> <input type="radio" id="option1" name="answer" value="1"> <label id="label1" for="option1">10 m<sup>2</sup></label><br> <input type="radio" id="option2" name="answer" value="2"> <label id="label2" for="option2">12.5 m<sup>2</sup></label><br> <input type="radio" id="option3" name="answer" value="3"> <label id="label3" for="option3">50 m<sup>2</sup></label><br> <input type="radio" id="option4" name="answer" value="4"> <label id="label4" for="option4">56.25 m<sup>2</sup></label><br> <input type="radio" id="option5" name="answer" value="5"> <label id="label5" for="option5">62.5 m<sup>2</sup></label><br> <input type="radio" id="option6" name="answer" value="6"> <label id="label6" for="option6">75 m<sup>2</sup></label><br> <input type="submit"> </form> <p class="hidden" id="answerGiven"></p> <p class="hidden" id="correct"><strong>That's correct!</strong></p> <p class="hidden" id="incorrect"><strong>Sorry! That's not the right answer!</strong> Try again...</p> <p class="hidden" id="nextQuestion"></p> <script> document.addEventListener('DOMContentLoaded', function() { checkAnswer('5'); }); </script> </body> </html>
This follows the same pattern as index.html but notice that the action for my form on line 12 is now “question3.html” – I need to make sure I update that for each page or I will send the answer to the wrong page!
Also note that I’ve updated the call to the checkAnswer function on line 28 so that it sends the correct answer for this question (‘5’).
Now that I’ve equipped question3.html with this functionality, my finalpage.html file serves little purpose, so I’ll delete it. Notice, however, that on line 25 I’ve still included an empty paragraph element with an id of “nextQuestion”. This is because the JavaScript function is expecting to find an element with this id, so I include an empty element here to prevent errors. Also, if I do decide to add further questions in future then I simply need to add the link to the next question here.
When I now open question3.html in a browser, I find it functions exactly as index.html does. Note that when I use innerHTML to get the content of the selected answer’s label element, this also includes the sup element, so the answer displays correctly.
So, now I can update question2.html to match index.html as well, right? Well, no. If you review question2.html you’ll recall that, while questions 1 and 3 expect a single correct option to be selected, question 2 expects multiple correct options to be selected. This means I can’t use radio buttons for question 2. The solution is to use another type of input element called checkbox.
I updated question2.html to look like this:
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>General Knowledge Quiz - Question 2</title> <link rel="stylesheet" href="css/quiz.css"> <script src="js/quiz.js"></script> </head> <body> <h1>General Knowledge Quiz</h1> <p class="question">Which of the following are national languages of Switzerland?</p> <form action="question2.html" method="GET"> <input type="checkbox" id="option1" name="answer1"> <label id="label1" for="option1">English</label><br> <input type="checkbox" id="option2" name="answer2"> <label id="label2" for="option2">French</label><br> <input type="checkbox" id="option3" name="answer3"> <label id="label3" for="option3">German</label><br> <input type="checkbox" id="option4" name="answer4"> <label id="label4" for="option4">Italian</label><br> <input type="checkbox" id="option5" name="answer5"> <label id="label5" for="option5">Romansh</label><br> <input type="submit"> </form> <p class="hidden" id="answerGiven"></p> <p class="hidden" id="correct"><strong>That's correct!</strong> Now try the next question...</p> <p class="hidden" id="incorrect"><strong>Sorry! That's not the right answer!</strong> Try again...</p> <p class="hidden" id="nextQuestion"><a href="question3.html">See question 3</a></p> <!-- <script> document.addEventListener('DOMContentLoaded', function() { checkAnswer('3'); }); </script> --> </body> </html>
You can see that, for this question, I’m now using the “checkbox” type of input, instead of “radio”. I’m also giving each option a unique name attribute and I’m not setting a value.
For now, I’ve enclosed the JavaScript at the end in an HTML comment block to stop it from running. I’ll come back to this in a moment.
When I open the page in a browser, I can see that Ie now get square checkboxes instead of round radio buttons, and the browser will allow me to select as many of these as I wish at the same time:
When I select some, but not all, of the answers and then click the Submit button, in the address bar, I can see that the form has reported only the answers I selected:
Clearly my current JavaScript function isn’t going to work with this question format. I could write a completely new function to handle this question type, but a lot of the functionality, e.g. display the correct or incorrect feedback, would be exactly the same. A better approach is to adapt the function I have so it works with both question types.
I changed the JavaScript function to look like this:
function checkAnswer(questionType, correct, numOptions) { let formValues = new URLSearchParams(window.location.search); let correctFeedback = document.querySelector('#correct'); let incorrectFeedback = document.querySelector('#incorrect'); let nextQuestion = document.querySelector('#nextQuestion'); let answerGiven = document.querySelector('#answerGiven'); if (!formValues.entries().next().done) { let answer = ''; let answerString = ''; if (questionType == 'multi') { for (let i = 1; i <= numOptions; i = i + 1) { if (formValues.has('answer' + i)) { let commaString = answer.length > 0 ? ', ' : ''; answer += commaString + i; answerString += commaString + document.querySelector('#label' + i).innerHTML } } } else { answer = formValues.get('answer'); answerString = document.querySelector('#label' + answer).innerHTML; } answerGiven.innerHTML = 'Your answer was ' + answerString; answerGiven.classList.remove('hidden'); if (answer == correct) { correctFeedback.classList.remove('hidden'); nextQuestion.classList.remove('hidden'); } else { incorrectFeedback.classList.remove('hidden'); } } }
You can see that I am now asking for three arguments rather than 1. These are:
- questionType – this is a string that will be set to “single” if the question is a single-answer type and “multi” if the question is a multiple answer type.
- correct – this is a string that will contain the numbers of the correct options, with each separated by a comma and a space, so the correct string for question 1 would just be “3”, while the correct string for question 3 would be “2, 3, 4, 5”
- numOptions – this is a number indicating the total number of answer options in the question.
Let’s see how these are used. If the question is a multiple answer type, which I check for on line 12, I need to check all the possible answer options in turn to see which of them the user has selected. I do this with a programming structure known as a loop. The loop starts on line 13 and ends on line 19. There are several types of loop in JavaScript and I’ll be using all of them in this series. This one is a for loop.
The for loop is generally used when I want a loop that is controlled by a counter. Let’s say you were dealing cards at a card game and each player was to receive a hand of seven cards. Let’s break down the process:
- You’d start your count at 1, for the first card
- You’d deal a card to each player
- You’d add 1 to your count, so it’s now 2
- You’d mentally check if you’ve dealt enough cards to each player
- You haven’t, so you’d deal another card to each player
- You’d add 1 to your count, so it’s now 3
- … and so on….
- until the count was 7 then you’d deal one more card to each player and stop.
There are four important elements in that pattern:
- What you start your count with: 1 (seems like an obvious choice, but you could have started at 7 and counted down).
- What you do for each count: deal a card to each player.
- How much you add to or subtract from your count: 1 in this case (but it would be -1 if you were counting down).
- What condition you check for to know when to stop: You’ve dealt seven cards to each player.
A for loop in JavaScript is designed to handle exactly this sort of pattern. The first line of the loop, for (let i = 1; i <= numOptions; i = i + 1), handles three of these things:
- What you start your count with: let i = 1, designates a variable, i, as a counter and starts it at 1
- What condition you check for to know when to stop: i <= numOptions, tells the loop to continue while the counter is still less than or equal to the total number of answer options.
- How much you add or subtract to your count: i = i + 1. adds 1 to the variable i. When our variables contain numbers we can do arithmetic with them, so i = i + 1, means take the number that’s currently in the variable i, add 1 to it and put the result back into i.
The final part, what you do for each count, is the code between the curly braces on lines 14 to 18. So, if my question had 4 answer options, this code would be run four times.
You’ve seen, for a multiple answers type question, that the form reports only the answers that the user selects. So each time through the loop I am checking for a different answer option. The has function of a URLSearchParams object returns true if it can find a parameter with that name and false if it doesn’t. I check for this with the if statement on line 14. Notice how I’m constructing the parameter to look for by using the counter value from the loop. So the first time through the loop I’d be looking for “answer1”, the second time for “answer2” and so on.
If I find a parameter with that name, it means the user has selected that option. If so, I need to do two things:
- add that answer option number to a list of selected options (line 16)
- add the answer option to a string of answers that I will display to the user (line 17).
You are probably wondering what this line does:
let commaString = answer.length > 0 ? ', ' : '';
Basically, when I am constructing both the list and string, if the item I’m adding is not the first item in the list or string, then I need to add a comma and a space before it. This line uses something called a ternary operator. It’s structured like this:
condition ? thing to return if true : thing to return if false
It’s a shorcut for this:
let commaString; if (answer.length > 0) { commaString = ', '; } else { commaString = ''; }
I use this in lines 16 and 17. Note that, while each of these variables is empty, then commaString will also be an empty string, so nothing would get added. But if the variables are not empty then “, ” will get added before the new item, ensuring that each item is separated by a comma and a space.
Line 16 constructs a list of all the options selected by the user. So if I selected options 1, 3 and 4, this variable would be set to “1, 3, 4”.
Line 17 also constructs a list of all the options selected by the user, but, as before, it gets the innerHTML of the corresponding labels, so this variable will end up containing something like “English, German, Italian”.
If the question is a single answer type then I just use my previous version of the code in lines 21 and 22 to set the answer and the text of the selected option.
The rest of the code should be familiar to you, with the exception of line 9, if (!formValues.entries().next().done). This replacesmy previous if statement, if (answer != null). I can’t use that any more because it will only work with single answer type questions. Instead I’m using a new function of the URLSearchParams object called entries. This function returns something called an iterator. An iterator enables me to iterate through a list of items. To get the next item in the list I use the next function. The next function has a property done, which is set to true if there no more items in the list and false if there are. So by chaining all these things together with formValues.entries().next().done, I’ll instantly get true if the list is empty (i.e. the user has not yet answered the question) and false if it has at least one value (the user has answered the question).
Because I want the code to run only if the user has answered the question, I need to invert this outcome. To do this I use the logical not operator (!). This basically turns true to false and false to true. I’ll be using more logical operators later in this series.
Before this revised code will run, I need to amend the lines that call the function in each HTML file to provide all three of the arguments that are now required. I changed line 26 in index.html to be:
checkAnswer('single', '3', 4);
In question2.html, I first removed the comment block from around the script element, then changed line 26 to be:
checkAnswer('multi', '2, 3, 4, 5', 5);
And in question3.html, I changed line 28 to be:
checkAnswer('single', '5', 6);
I now find that all three questions are marked correctly, and correctly report my selected answers:
My quiz is really shaping up nicely, but there’s one fairly huge problem with it. Every time I want to add a new question, I have to create a completely new HTML file, which is a lot of effort. Also, if I subsequently wanted to remove a question or reorder questions, I’d need to manually adjust several links.
In the next post I’ll look at ways of fixing this. In the meantime, here’s a bit of a tougher challenge I need to solve. At the moment, if I answer a question correctly, the Submit button is still visible and active. Ideally, once the question is answered correctly, I want the Submit button to disappear, so the only action available to the user is to move on to the next question.
I need to find a way to do this. I’m thinking. once I’ve got an element’s classList, I can also use a function to add a class to the element. I’ll show my full solution in the next post.
Be First to Comment