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:

Do you like cats?

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>
Do you like cats?
Do you like dogs?

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>
Do you like cats?
Do you like dogs?
Do you like rats?

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

video of the solution

(That video is somewhat out of date, since I've added the media query feature. I'll demo the new solution in class.)

screenshot of two multiple choice questions
Here is a screenshot of two questions contributed in Spring 2024

And here's another:

screenshot of two multiple choice questions, answered
Here is a second screenshot after I've tried to answer both questions

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.

formatted page of questions
A nicely formatted page of questions

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:

questions marked right or wrong using color and text
A nicely formatted page of answered questions, marked right or wrong using color and text.

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:

  1. jQuery; the template file already has this
  2. my bounds plugin; the template file has this, too
  3. the movie-questions.js that I gave you above. You'll copy this file and then write the script tag to load it.
  4. 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 the script 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 the event.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