Assignment: Quizzes¶
We regularly take multiple-choice quizzes using Sakai. In this assignment, you will write an application that formats quiz questions into a bunch of radio button sets and allows the person to answer the questions and "grade" their answers.
Goal¶
In this assignment, you will dynamically create quiz questions from a list of information, and you will add a single event handler to which you will delegate all the grading.
Don't panic. This is not an easy assignment, but neither is it grueling. The effort is mostly conceptual. The actual number of lines of code is quite small. My solution was less than 100 lines of code, but it is not important how short your code is. (I could probably do more better documentation and commenting.)
Dependencies¶
This assignment is based only on the material up to and including Dynamic DOM, in which we learned to dynamically add a elements to the page. It also requires event delegation.
You'll have to create radio buttons, so look back at the lessons on that: radio buttons.
The JavaScript for this assignment is almost entirely based on the "Dynamic DOM" reading: dynamic DOM.
When you get to the "grading" portion of the assignment, you'll have to use Event Delegation
More Information on Radio Buttons¶
For best accessibility, radio button groups should be surrounded by a
fieldset
element, which has a legend
element that describes the
group. See this information on radio group
accessibility
Here's an example:
<form class="eg_form">
<fieldset>
<legend>Do you like cats?</legend>
<label><input type="radio" name="cats" value="yes"> Yes, of course</label>
<label><input type="radio" name="cats" value="no"> Sadly, no</label>
</fieldset>
</form>
Here it is in action:
Here's a subtle but helpful bit of information about radio buttons. We
already know that the set of buttons that are in a group are defined
by the shared name of the group. (In the example above, both radio
buttons have name="cats"
, which is what makes them a group.) Any
button with the same name is in the same group. The extra bit of
information is that this rule is applied per form element.
Let's make that concrete with some examples:
One Form, Two Groups¶
The following form has two separate Yes/No questions in them, so the
two questions have to use separate names. Here, we called them
name="cats"
and name="dogs"
.
<form class="eg_form">
<fieldset>
<legend>Do you like cats?</legend>
<label><input type="radio" name="cats" value="yes"> Yes, of course</label>
<label><input type="radio" name="cats" value="no"> Sadly, no</label>
</fieldset>
<fieldset>
<legend>Do you like dogs?</legend>
<label><input type="radio" name="dogs" value="yes"> Yes, of course</label>
<label><input type="radio" name="dogs" value="no"> Unfortunately not</label>
</fieldset>
</form>
That's the normal case. But look! The cats
group in the two-question
form is separate from the cats
group in the earlier example. Try
it! Click "no" for one cat question and "yes" for the other. That's
because they radio groups are in separate forms, so the groups are
in independent, even though they happen to have the same name.
Two Forms, Two Groups¶
That means that if we are willing to have different form elements, we
can reuse a radio group name. So, if we wanted to ask a bunch of
yes/no questions, we could do it like this, in which all the questions
have the same name (name="yes_no"
) but they are independent because
they are in different forms.
<form class="eg_form">
<fieldset>
<legend>Do you like cats?</legend>
<label><input type="radio" name="yes_no" value="yes"> Yes, of course</label>
<label><input type="radio" name="yes_no" value="no"> Sadly, no</label>
</fieldset>
</form>
<form class="eg_form">
<fieldset>
<legend>Do you like dogs?</legend>
<label><input type="radio" name="yes_no" value="yes"> Yes, of course</label>
<label><input type="radio" name="yes_no" value="no"> Unfortunately not</label>
</fieldset>
</form>
<form class="eg_form">
<fieldset>
<legend>Do you like rats?</legend>
<label><input type="radio" name="yes_no" value="yes"> So cute!</label>
<label><input type="radio" name="yes_no" value="no"> Ick! No way!</label>
</fieldset>
</form>
You should use fieldset and legend in your solution. You may find it helpful to use the information above about names of radio groups.
Reference Solution¶
Because the HTML and CSS is an important part of this assignment (though not the hardest part), I'm going to omit the obfuscated solution (since I don't know how to obfuscate HTML and CSS). Here's a
(That video is somewhat out of date, since I've added the media query feature. I'll demo the new solution in class.)
And here's another:
Notice that the radio button groups are independent and when I pressed the "grade" button, I get feedback both in text and in color. That's what you are implementing.
Operation¶
The app will work like this:
When the page loads, a global variable will be defined that contains a list of multiple-choice questions in JSON format. Here are some example questions based on the movies. You're welcome to use those, or write your own.
Each question looks like this:
var questions = [
{Q: "Who starred in Casablanca?",
A: "Charleton Heston",
B: "Matt Damon",
C: "Harrison Ford",
D: "Humphrey Bogart",
ANS: "D"
},
...
];
Each question is represented by a single JS object literal (dictionary) with the following keys: there's the question (Q), four answers (A-D) and the answer (ANS).
Your code will iterate (use .forEach()
) over that list of questions
and format them onto the page.
Each question will be nicely formatted as a paragraph and a group of four radio buttons for that question. There will also be a button to grade each question. Here's the screenshot from earlier.
Once the page is loaded, the user can choose radio buttons and click on the "grade" button. If their choice is correct, the question is marked correct by adding a "correct" CSS class (that is, a CSS class named "correct") and inserting an appropriate message. Similarly, if it's incorrect, it's marked by adding a "wrong" class and an appropriate message. My classes changed the border and background color, like this:
High-Level Overview¶
Because I want you to write the HTML and CSS, you should copy the
template file for your HTML. Also copy the movie-questions.js
,
though of course you can create your own questions. (In fact, that's
encouraged! Quiz me while I'm grading.)
cd ~/public_html/cs304-assignments
mkdir quizzes
cp ~cs304/public_html/readings/template-jq.html quizzes/quiz.html
cp ~cs304/public_html/assignments/quizzes/movie-questions.js quizzes/
My code has an HTML file with an empty container for questions. My HTML file loads these JS files:
- jQuery; the template file already has this
- my bounds plugin; the template file has this, too
- the
movie-questions.js
that I gave you above. You'll copy this file and then write thescript
tag to load it. quiz.js
which is the main driver. It builds the quiz questions and defines the event handler to do grading. You'll write this file from scratch and also write thescript
tag to load it.
The .js
files should probably be loaded in that order.
The Main File¶
The quiz.js
file puts the pieces together. It does two things:
It maps over the JSON array of questions, creating DOM elements for
each question and appending the top DOM element to the document. Use
forEach()
and a callback function. Remember that
forEach
invokes the callback function with (a) the element, (b) its index in
the array, and (c) the array. You should use the two-arg version of
the callback function.
The quiz.js
file must also add an event handler to some appropriate
ancestor of all the questions, delegating the click-handling for all
the "grade" buttons. That click handler should determine whether the
user got the question right, and add the appropriate class (and
removing the other class, if any).
It's important that this be a delegated handler. See the reading on delegation
Event Handling¶
The event handling is not a lot of code, but it's tricky. We learned about this kind of event handling in the Dynamic DOM reading.
Here are some sub-problems to solve:
- How will the event handler know what the answer to this question is?
Remember that the one handler is responsible for grading all the
questions. I suggest storing the correct answer in an element, using
a
data-answer
attribute. That will affect the HTML for the construction of the DOM elements. - How will the event handler find the element that has the
data-answer
? This will undoubtedly require searching the tree, probably upward from theevent.target
using the jQuery.closest()
method. See closest - What will the event target be? What click events will be delegated to this handler?
- How will the handler know what answer the user gave? We saw how to
use the
.val()
method in class. You might consider using.find()
as well. See find
Once you've answer those questions, I think you'll find that the code is something like:
var correct = [code to find correct answer]
var given = [code to find answer user gave]
if( correct === given ) {
[code for marking it correct]
} else {
[code for marking it wrong]
}
The grade
Button¶
Note that the button used for grading is not a radio button but a "regular" button. There are two ways to create such a button:
<input type="button" value="grade">
<button type="button">grade</button>
They look like this:
The second way is nice if you want to wrap the button
tag around,
say, an image (turning the image into a clickable button). However,
there's a trap. If you do the following, you'll inadvertently create
a submit button, since that's the default:
<button>grade</button>
But in this assignment, we don't want to be submitting these forms. So use a "button button", not a submit button.
Testing the Event Handler¶
Testing the event handler is difficult, but you can do it. The main idea is to have something that you can test in the JS console, so I strongly suggest keeping the actual event handler small, and punting most of the work to another function that you can easily test:
$(selector_for_some_static_ancestor).one()
.on('click',
'selector_for_dynamic_descendant',
function (event) {
foo(event.target);
});
function foo(some_button) {
// code to process a button click
}
So, the function foo
(clearly, you should name it better that that)
is something you can test incrementally in the JS console.
The argument to foo
will be one of the GRADE buttons. It will not
be any radio button. The foo
function will be invoked, via the event
handler, when the user clicks on one of the GRADE buttons.
How can you get one of these buttons for testing purposes? Here's a way to get one of them:
var b = $("button")[i]; // where i is 0, 1, 2 or 3
That incantation works because "button"
is a CSS selector that will
match the four GRADE buttons (you should check that that's true), so
the jQuery result set will have 4 elements in it, and then you just
index into it to get one of them. So b
will be one of the four
buttons.
You can then test foo
by evaluating foo(b)
.
Hints¶
I found I had to use all of the following jQuery methods. You don't have to use the exact set, but this might be helpful.
.clone()
to clone a template.empty()
to make a DOM element empty by removing all its descendents.attr()
to get an attribute's value.closest()
to search up the tree.find()
to search down the tree.val()
to get the value of an input.text()
to set the text in an element.addClass()
to add a CSS class to an element.removeClass()
to remove a CSS class to an element if it's there.append()
to add a child to a parent element
No dynamic element should have an ID. Some of your static elements might have an ID; that's your choice. I believe you should treat this as a general rule: IDs are only for static things where you can look at the HTML and say, "yeah, that thing is unique. Let me label it with a unique identifier."
Ajax: the Final Touch¶
Now that you have successfully handled my four movie questions, you should load the crowd-sourced ones from your classmates. This will involve understanding asynchronous functions. We discussed how to do that in class and we will again. Things to remember:
no function will be able to return the data. You will have to either pass in a callback function or deal with the data in the asynchronous function.
Let's first pull out the idea of the function that handles the data. It gets the data as an argument, and it will do two helpful things with it: print it to the console and set it to a global (so you can play with it if you want).
var globalData;
function handleData(data) {
console.log(data);
globalData = data;
}
Here's a some code using jQuery and the callback function
const crowdURL = ('https://cs.wellesley.edu/cs304node/questions/questions/');
function getUsingJquery() {
$.get(crowdURL, handleData);
}
Try it!
Next, let's see it with Promises and the Fetch API:
function getUsingPromises() {
fetch(crowdURL).then((resp) => resp.json()).then(handleData);
}
Try that, too!
Finally, let's see it with async
and await
:
async function getQuestions() {
let resp = await fetch(crowdURL);
let data = await resp.json();
handleData(data);
}
Your page should have a button on it that loads and renders the crowd-sourced questions, replacing the movie questions.
Note that most of the crowd-sourced question have an author. You are welcome to display that information as well.
Checklist¶
- You and your partner's names at the top of every JS file
- Every js file has
"use strict";
at the top - All functions properly documented and commented
- Neat, clear code
- Lines not excessive in length
How to turn this in¶
Do all your work in a new folder in your cs304-assignments
folder
called quizzes
. Upload your files to Gradescope (so I can comment on
them) and you're done! Here's a reminder of the drop
command:
drop cs304node queriesHwk.js
Tutors/Graders¶
You can use the password to view the quiz