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. We won't keep track of the score, but if we wanted to, we could use kind of code we did with Rock-Paper-Scissors.
Goal¶
In this assignment, you will use grid systems, you will dynamically create quiz questions from a list of information, and you will add an event handler to which you will delegate all the grading.
Dependencies¶
This assignment is based only on the material up to and including
Dynamic DOM, in which we learned to dynamically add a checkbox to the
page. It also relies on Bootstrap and Grids. It requires you to use
the class
syntax for OOP, like we did with bank accounts, but not
the OOP with inheritance. It requires event delegation.
You'll have to create radio buttons, so look back at the lessons on that: radio buttons and class review
It also uses the grid HTML/CSS code that we learned in class: grids and the class review
I'm going to allow you to use either the new grid CSS or the Bootstrap grid system. There's a small difference in the HTML and CSS, and either solution is fine. The description below supposes the new grid CSS.
- The JavaScript for this assignment is almost entirely based on the "Dynamic DOM" reading: dynamic DOM.
- In particular, you should look at the
addRow
method from the Checklist Class section, and - and the
makeRowElement
function from the Helper Functions section.
Your code will not be precisely either of those; we've organized things a little differently. But those two functions explain a lot of the coding you will need to write.
When you get to the "grading" portion of the assignment, you'll have to use Event Delegation.
The only thing that isn't in that reading is the two-column grid-based layout, which is from the reading on Bootstrap
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.)
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. The page will have two sections of
questions, using the grid system; either laid out horizontally (for
wider screens) or vertically (for narrower screens). If the page has a
vertical layout, the first half of the questions goes above the second
half (obviously) and if the page has a horizontal layout, the first
half of the questions go in the first column (to the left) and the
second half of the questions in the second column (to the right).
Don't think that you'll be dynamically creating grid elements. That's probably possible, but I didn't. Instead, I just had two pre-specified (static) grid elements, and I dynamically added questions to those existing grid elements.
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 a screenshot:
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 grid CSS, you should copy the template file for your HTML, though you an adapt the HTML to use Bootstrap. 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/cs204-assignments
mkdir quizzes
cp ~cs204/pub/readings/template-jq.html quizzes/quiz.html
cp ~cs204/pub/assignments/quizzes/movie-questions.js quizzes/
My code has an HTML file with two empty containers for questions (the left column and the right column). My HTML file loads five 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. Question.js
which defines theQuestion
class. (Note the capital Q in the filename, because this file describes theQuestion
class.) The class will have a constructor and one method namedaddToDOM
. You'll write this file from scratch and also write thescript
tag to load itquiz.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 five .js
files should probably be loaded in that order.
Implementation¶
We should separate our code into clear modules. But I am not asking for modules or IIFEs in this assignment.
Scaffolding¶
The following is a step-by-step implementation of a simplified version of the assignment: dynamic list
Question class¶
Question.js
will define a new class of object called Question
. It
will have:
- one instance variable to store the DOM element
- a constructor that builds and stores that DOM element
- a method that adds that DOM element to the page.
The constructor will take an object literal like the one above (the
"who starred in Casablanca" question), describing just one question,
and an integer (the question number). The constructor will create the
HTML structure for the question, similar to the MakeRowElt
function
from CoffeeRun. The constructor will not add the structure to the DOM,
but instead save it in an instance variable. The HTML structure will
be what I've described as "off stage".
The constructor should not reference the questions
array at
all. In principle, we should be able to create questions from several
arrays, or from a form that people fill out, or some other source. The
fact that the second argument to the constructor happens to be an
index into the questions
array is a coincidence. Do not use that
array anywhere in the Question.js
file.
The Question
class will also have an addToDOM
method that adds the
structure that was saved in an instance variable) to the document at
the given destination. The destination is specified by the argument to
the addToDOM
method.
To add something to the document, we typically use the jQuery
append()
method:
$(somplace_in_the_document).append(new_thing_to_add);
There are some pictures of these steps later in this document.
The Main File¶
The quiz.js
file puts the pieces together. It does two things:
It maps over the JSON array of questions, creating objects for each
question and appending the the DOM element to the document in either
the first or second column. The first half of the list should go in
the first (left) column, and the second half of the list should go in
the second (right) column. 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
The Question Class Visualization¶
It might help to visualize what the constructor and the addToDOM
method do.
First, the constructor:
Later, we add the off-stage HTML to the page:
Question Construction¶
Constructing the DOM element for a question is a fair amount of code, but
not conceptually difficult. Look at the Row
constructor for some
inspiration.
I strongly suggest that you work backwards from the HTML. That is:
- Write out the HTML that your function needs to create. Actually code it up in your HTML file. Test that it works. You can leave it in there, and dynamically add other questions after it. Or you can delete it or comment it out, later.
- Identify the elements and their parent-child relationships.
- Identify which parts are static and which parts will depend on information from the question (object literal).
- Construct the elements and use
.append()
to establish the parent-child relationships. - Store the branch in an instance variable for later.
Note that each set of radio buttons must have a shared name in order
to work as a group. I suggest "Q"+i
where i
is the index of the
question in the array. Therefore, the constructor will have two
arguments: the object literal describing the question and the index.
You will have a non-radio button for triggering the grading behavior. It
should be a regular button, not a submit
button, so something like this:
<button type='button'>grade</button>
You can, of course, add classes and attributes if you'd like.
The Question
object must also have a method called addToDOM
that takes
an argument that is a DOM element to append the question to. This is a
slightly different interface than CoffeeRun uses.
Note: even though radio buttons are usually contained within a
form
element, we will not be sending these to a back-end server, so
there's no use in creating a form. Furthermore, there are subtle and
weird things that can happen if you do. (I'm happy to explain if you
want, but I don't want to clutter this document with that
explanation. See me to learn more!)
Testing the Constructor¶
Because of the way we've separated the role of the constructor and the
addToDOM
method, testing the constructor is a little
difficult. Here's an approach that might help:
I suggest putting all/most of the code into a helper function that you can easily call in the JS console:
function helper(questionDescription, num) {
...
return questionDiv;
}
That helper function will not reference any global variables or store into any global variables. It will be purely functional.
Then you can test it like this in the JS console:
elt = helper( questions[0], 0 );
$("body").append(elt);
and see what the result looks like by using your browser and the inspector.
Once that's all working, your constructor can just use the helper
function but instead of appending the return value to the body, it
stores the element in an instance variable and the addToDOM
method
can add it to the page. Note that using the helper function this way
will make the constructor very short. The addToDOM
method is also
very short, so the whole class
definition is only a handful of lines
of code. But that's okay.
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 Question construction. - 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]
}
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).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:
.attr()
to get an attribute.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."
Layout¶
I'd like you to learn a bit of grid systems, so I'd like you to achieve the following effects. First, here's a screenshot of my solution:
- the container is 80% of the width of the page, centered
- it has a gray background color
- it has two columns with a 2em gutter between them
- each column has a silver background color and a 2px solid green border. (One consequence of this is that you can only see the gray background color in the 2em gutter between columns.)
- each question has a 2px solid black border, white background color and some reasonable nice margins and padding
- the possible answers to each question are laid out vertically
Note that clicking on the actor name should choose that button. You'll
achieve that with proper use of label
Media Query¶
Use a media query so that for narrow screens (less than 768px) it's a one-column layout, and for wider screens (greater than or equal to 768px) it's a two-column layout. Or, you can use Bootstrap's media queries.
Modularity¶
Making the questions and adding the event handler should be done with functions.
Each button of the radio button groups is identical in structure. You should define a function that creates one and invoke it four times. That function should not be global.
How to turn this in¶
Do all your work in a new folder called quizzes
. Do the Gradescope submission.
Tutors/Graders¶
You can use the password to view the quiz
Time and Work
Finally, when you have completed the assignment, make sure you fill out the Time and Work report. That report is required.