Advertisement

Build a Spiffy Quiz Engine

by

The first official Nettuts+ quiz was a massive success with an impressive number of developers participating and evaluating their knowledge. There were a non-trivial number of comments asking how the mini quiz engine was built. And lo and behold! That's what we're gonna learn today.


A Word from the Author

Quizzes are a great way to engage the community -- just take a look at our recent quiz. The problem is that, in this day and age, people expect quizzes to be fairly interactive and my Google-fu failed strongly when searching for a system that eases the process. That's when I decided to build myself, and Nettuts, a super simple quiz engine, one that I playfully call kroggy.

I believe a demo is worth a thousand words. Hit the demo and try it out for yourselves.

Today, we are going to look at how to implement this with, you guessed it right, our favorite JavaScript library, jQuery. Interested? Let's get started right away!


Design Goals

The design goal for this version are incredibly simple. I'll walk you through the points I had in mind.

  • We'll something that looks slick and elegant -- one that invites the user to take the quiz.
  • This is no place for a long list of questions with radio buttons and labels. We'll need to implement a simple slider of some sort to view each question individually.
  • Questions will be evaluated at the end of the quiz instead of immediately. This way the reader can focus on the quiz taking process instead of having to pause each time to evaluate the result.
  • A simple progress bar at the bottom to display the user's progress.
  • The correct answers can live in either the JavaScript or the HTML. This isn't the SATs -- no need to introduce more complexity to our fledgling codebase.

Some notable features that I'm opting out of:

  • No post quiz reviews. Doing so would have us scraping our own HTML for data and then re-rendering it. Not fun! And definitely not within the scope of today's tutorial.
  • We are not going to wrap this up into a plugin. We're better off focusing on the actual development portion of the functionality. There are plenty of content on how to refactor code into a jQuery plugin.
  • As an extension of the above point, it also means that the HTML for the quizzes will be pre-written by us -- it won't be generated and inserted dynamically.

That's about it, I guess. Let's move on to how we're going to accomplish this.


Plan of Action

We'll now need to map out what needs to be done in a specific order.

  • Step 1: Create the HTML markup for the base quiz engine and style it accordingly.
  • Step 2: When a user has selected an option and clicks the next button, just silently move to the next question. If no option is selected, throw an error.
  • Step 3: When the user is on the final question and clicks on the next button, raise hell and evaluate results like so.
  • Step 4: Find out which option the user has selected and match it to the list of predefined answers. Return a simple array so we can evaluate the results later and present some data in the final screen.

Ok, that sounds reasonably easy.These are the basic steps in creating this functionality. Of course there are few other small things but I’ll explain them as we go along.

Now let's dig into some code and get our hands dirty. First, the HTML.


Core Markup

The markup is one of the pillars that holds this engine up so we'll go through each part individually with the entire code shown at the end to show the big picture.

<div id="main-quiz-holder"> 
</div>

A div element with an ID of main-quiz-holder will be the main container within which we're going to place our entire HTML code -- all the code below goes inside this div element.

 
<div class="questionContainer"> 
</div>

Next up, let's create a sample slide of the quiz. Remember that we're going to create a super simple slider to hold our individual questions. Each slide will be housed with a div element with a class of questionContainer.

Before we go into the markup of a quiz slide, let's handle a few formalities.

 
<div id="intro-container" class="questionContainer"> 
	<a class="btnStart" href="#"><img src="img/start.png" /></a>    
</div>

The above code is for the intro container, the one which is displayed asking the user to take the quiz. We're just adding an ID of intro-container to it to help us style it better later.

Inside the div, we merely have a single splash image wrapped by an anchor element.

 
<div id="results-container" class="questionContainer"> 
    <div id="resultKeeper"></div> 
</div>

And the results slide. Nothing special here: we assign a special ID and place an empty div inside it to hold our results. We'll populate this element down the road when we're evaluating the results.

If you noticed the class of questionContainer for both this and the slide above, gold star for you! We're adding this class since both these containers are part of the slider.

 
<div id="progressKeeper" ><div id="progress"></div></div> 
<div id="notice">Please select an option</div>

And finally, a little housekeeping. We create div elements for the progress bar container, the actual bar itself and the notice that'll be shown in case the user doesn't select an option.

With this, our skeleton markup is over.


Markup for Each Question

Ahh, the meaty part of the markup is upon us. These sections may look complicated but as always, I'll break it down into smaller chunks and walk you through the code.

 
<div class="questionContainer"> 
	<div class="question">Question</div> 
 		<!-- UL with options --> 
        <div class="btnContainer"> 
				<!-- Internal navigation links --> 
        </div> 
	</div> 
</div>

As mentioned previously, we'll be wrapping each of our quiz slides with a div element with a class of questionContainer. Inside, we have a div element with a class of question that contains each of our questions.

We follow it up with an unordered list containing the possible answers to the question and a div acting as the container for the navigation elements [previous/next]. Make a note of the class name of each of these sections.

 
<ul class="answers"> 
    <li> 
        <label><input data-key="a" type="radio">lend structure to the document</label> 
    </li> 
    <li> 
        <label><input data-key="b" type="radio">mold the presentation of the document</label> 
    </li> 
    <li> 
        <label><input data-key="c" type="radio">script the interactions on the page</label> 
    </li> 
    <li> 
        <label><input data-key="d" type="radio">You're crafty! This is a trick question.</label> 
    </li> 
</ul>

Expanding on our unordered list, each li element has a label element containing a single radio button. Notice that we're assigning each radio a data-key attribute? We'll look at it a bit later.

 
<div class="btnContainer"> 
	<div class="prev"><a class="btnPrev" href="#">Prev</a></div> 
	<div class="next"><a class="btnNext"  href="#">Next</a></div> 
	<div class="clear"></div> 
</div>

And finally, the navigation container. Nothing fancy here. We have a main div with a class of btnContainer that acts as the container. Inside we have an anchor wrapped by a div element. The div is merely for styling purposes so feel free to discard that in your implementation.

Keep in mind that the previous and next buttons have to be inserted logically. You wouldn't want a previous button on the first actual quiz slide while the final slide needs to have the button to trigger the evaluation process.

Instead of the next button the final slide needs to have the following:

 
<div class="next"><a class="btnShowResult" href="#">Finish</a></div>

And that wraps up the HTML portion of our quiz slides. You'll notice that we haven't tackled the HTML for the quiz results. We'll tackle that during the JavaScript phase. For now, let's move on to the presentation.


Final HTML

 
<div id="main-quiz-holder"> 
	<div id="intro-container" class="questionContainer"> 
		<a class="btnStart" href="#"><img src="img/start.png"/></a> 
	</div> 
	<div class="questionContainer hide"> 
		<div class="question"> 
			CSS is used to... 
		</div> 
		<ul class="answers"> 
			<li> 
			<label><input data-key="a" type="radio">lend structure to the document</label> 
			</li> 
			<li> 
			<label><input data-key="b" type="radio">mold the presentation of the document</label> 
			</li> 
			<li> 
			<label><input data-key="c" type="radio">script the interactions on the page</label> 
			</li> 
			<li> 
			<label><input data-key="d" type="radio">You're crafty! This is a trick question.</label> 
			</li> 
		</ul> 
		<div class="btnContainer"> 
			<div class="next"> 
				<a class="btnNext" href="#">Next</a> 
			</div> 
			<div class="clear"> 
			</div> 
		</div> 
	</div> 
	<div class="questionContainer hide"> 
		<div class="question"> 
			The C in CSS stands for? 
		</div> 
		<ul class="answers"> 
			<li> 
			<label><input data-key="a" type="radio">Crysis</label> 
			</li> 
			<li> 
			<label><input data-key="b" type="radio">Crocodile</label> 
			</li> 
			<li> 
			<label><input data-key="c" type="radio">Consistent</label> 
			</li> 
			<li> 
			<label><input data-key="d" type="radio">Cascading</label> 
			</li> 
		</ul> 
		<div class="btnContainer"> 
			<div class="prev"> 
				<a class="btnPrev" href="#">Prev</a> 
			</div> 
			<div class="next"> 
				<a class="btnNext" href="#">Next</a> 
			</div> 
			<div class="clear"> 
			</div> 
		</div> 
	</div> 
	<!-- More questions here --> 
	<div class="questionContainer hide"> 
		<div class="question"> 
			The * selector selects... 
		</div> 
		<ul class="answers"> 
			<li> 
			<label><input data-key="a" type="radio">every div element</label> 
			</li> 
			<li> 
			<label><input data-key="b" type="radio">only paragraphs</label> 
			</li> 
			<li> 
			<label><input data-key="c" type="radio">only parent elements</label> 
			</li> 
			<li> 
			<label><input data-key="d" type="radio">every element</label> 
			</li> 
		</ul> 
		<div class="btnContainer"> 
			<div class="prev"> 
				<a class="btnPrev" href="#">Prev</a> 
			</div> 
			<div class="next"> 
				<a class="btnShowResult" href="#">Finish</a> 
			</div> 
			<div class="clear"> 
			</div> 
		</div> 
	</div> 
	<div id="results-container" class="questionContainer hide"> 
		<div id="resultKeeper"> 
		</div> 
	</div> 
	<div id="progressKeeper"> 
		<div id="progress"> 
		</div> 
	</div> 
	<div id="notice"> 
		Please select an option 
	</div> 
</div>

At the end of this phase, our page looks like so:



Core CSS

Let's start making our quiz look sleek and attractive. First, the main container.

 
#main-quiz-holder { 
	margin: 0 auto; 
	position: relative; 
	background: #FCFCFC; 
    border:1px solid #dedede; 
	 box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -o-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -webkit-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -moz-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 border-radius: 2px; 
	 position: relative; 
	 width: 600px; 
	 font-family:  "Myriad", "Myriad Pro", "Helvetica","Segoe UI", "Lucida Sans Unicode", "Lucida Grande", sans-serif; 
}

Basically we're centering it, giving it a background and a border to make it stand out. The CSS3 properties may look a little confusing but it's mainly because of the browser specific properties. We're simply defining a box shadow for the container here -- nothing complicated once you learn the syntax.

 
#results-container, #intro-container { 
	width: 500px; 
	text-align: center; 
}

Let's define a width and center the contents of the intro and final slide. This way we don't have to style each individually since both of their contents will have to be centered anyway.

 
.questionContainer .question, h2.qTitle { 
    margin: 10px 0 20px 0; 
	 font-size: 26px; 
	 font-weight: normal; 
} 
h2.qTitle { 
	font-size: 32px; 
	margin-top: 30px; 
}

Let's style our titles and questions now. Since we want them to stand out amidst the other elements, we're bumping up the font sizes and giving them sizeable margins on either side. Rememeber, oodles of white space helps frame the content better.

 
#progressKeeper { 
    width: 553px; 
    margin: 0px 12px; 
	 box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -o-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -webkit-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -moz-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 border-radius: 2px; 
	 border:1px solid #dedede; 
	 position: absolute; 
	 bottom: 10px; 
	 left: 10px; 
}

On to the progress bar container now. The first thing you'll need to make a note of is the position: absolute declaration. We're adding this since we need this element to remain fixed within the parent container

The bottom and left properties specify that it should be fixed at the bottom of the container. The rest of the CSS is merely for styling purposes.

 
 #progress { 
    width: 0; 
	 height: 20px; 
  color: #4c4c4c; 
  background: #f6f6f6; 
  background: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#d4d4d4)); 
  background: -webkit-linear-gradient(#f6f6f6, #d4d4d4); 
  background-image: -moz-linear-gradient(top, #f6f6f6, #d4d4d4); 
  background-image: -moz-gradient(top, #f6f6f6, #d4d4d4); 
 }

And now the actual progress bar. We want it to be understated and yet prominent enough so I'm going with a slight grey gradient. As mentioned previously, this may look complicated but it's because of the relatively complex syntax of the CSS3 gradient property.

 
#notice { 
	position: absolute; 
	bottom: 40px; 
	right: 20px; 
}

notice is the div element that holds the error notice when the user screws up, say, when he hasn't chosen an option. To make things simpler, I've chosen to position it absolutely, just above the progress bar.


Question Slide CSS

 
.questionContainer { 
    width: 560px; 
	 min-height: 400px; 
    padding: 20px; 
	 overflow: auto; 
	 margin: auto; 
}

On to the slide container, questionContainer, first. We're defining a fixed width so that we can center it properly. We're also defining a minimum width so it doesn't break with fewer options. Finally, we're adding a bit of padding to improve the presentation and adding a overflow: auto declaration to deal with floated child elements.

 
.questionContainer ul.answers { 
    margin: 0px; 
    padding: 5px; 
	 list-style: none; 
}

Let's deal with the answers list next. We're merely removing the styling for the list by applying list-style: none and a little margin and spacing for better presentation.

 
.questionContainer ul.answers li { 
	padding: 5px 50px; 
	margin: 12px 0; 
	color: #4c4c4c; 
  -webkit-border-radius: 4px; 
  -moz-border-radius: 4px; 
  border-radius: 4px; 
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); 
  -webkit-box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
  -moz-box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
  background: #f6f6f6; 
  background: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#d4d4d4)); 
  background: -webkit-linear-gradient(#f6f6f6, #d4d4d4); 
  background-image: -moz-linear-gradient(top, #f6f6f6, #d4d4d4); 
  background-image: -moz-gradient(top, #f6f6f6, #d4d4d4); 
  border: 1px solid #a1a1a1;	 
}

The above is the styling for the individual list elements. The CSS can be split into 3 sections.

  • The first few lines are very basic -- we add a little margin and padding, specify a color for the text inside and add a little border radius.
  • Next up, we're using the CSS3 box shadow property to make it look better. Note the different prefixes for different browsers.
  • Finally, we're handling the background of the element. We're specifying a greyi-ish gradient to be displayed.
 
.questionContainer ul.answers li.selected { 
	background: #6fb2e5; 
  box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -o-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -webkit-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -moz-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
}

On to the styling for the li element when it has been selected by the user. We need it to really stand out to help the user experience. With that in mind, I'm opting for a bright blue background to be applied.


CSS for the Navigation Elements

 
.questionContainer .prev, .questionContainer .next { 
	height: 19px;  
	cursor: pointer;  
	padding: 5px 10px; 
   font-size: 16px; 
   padding: 5px 10px; 
  color: #4c4c4c; 
  border-radius: 4px; 
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); 
  background: #6fb2e5; 
  box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -o-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -webkit-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -moz-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
}

Let's go ahead and style the div elements holding the navigation elements. Most of the code above should be fairly explanatory. We're defining some padding to frame the text inside better, assigning it a color, increasing the font size a bit and adding some rounder corners. And finally, we're adding some CSS3 box shadows to make it look spiffy.

 
.questionContainer .next  { 
  background: #77d125; 
  box-shadow: 0 1px 5px #3caa00, inset 0 10px 20px #c9ffb6; 
   -o-box-shadow: 0 1px 5px #3caa00, inset 0 10px 20px #c9ffb6; 
   -webkit-box-shadow: 0 1px 5px #3caa00, inset 0 10px 20px #c9ffb6; 
   -moz-box-shadow: 0 1px 5px #3caa00, inset 0 10px 20px #c9ffb6; 
}

Since we want the next button to look different, we're overriding some of the styles from above. Here, I'm moving the main shade from blue to green.

 
.questionContainer .prev { float: left;} 
.questionContainer .next, .questionContainer.btnShowResult { float: right; } 
.questionContainer .clear { clear: both; }

This'll take care of the position for each of the buttons. We want the previous button floated to the left while the next and final buttons need to be floated to the right. That's precisely what the above code does.

 
.btnPrev { 
	padding-left: 24px; 
	background: url(img/back.png) left no-repeat; 
} 
.btnNext { 
	padding-right: 24px; 
	background: url(img/forward.png) right no-repeat; 
} 
.btnShowResult{ 
	padding-left: 24px; 
	background: url(img/confirm.png) left no-repeat; 
} 
.btnStart { 
	display: block; 
	margin: 40px auto 0 auto; 
} 
.btnContainer { 
	margin: 20px 0 30px 0; 
	padding: 5px; 
}

During the HTML phase you must have noticed that the buttons themselves have a parent div element with a child link element. We styled the parent elements in the previous code block. The above handles the child links.

Basically, the above code inserts the little graphic into the buttons. We're adding a little padding so the text is nicely offset. Notice how we change between padding-left and padding-right to style each element precisely.

The btnContainer itself gets a little margin and padding to position it where we want.


Styling the Results

Phew! Most of the CSS work is now behind us. Let's tackle the final piece of CSS -- the final slide which displays the results. Since you haven't seen the HTML, yet, it may be a little confusing but the CSS show be fairly generic and easy to parse.

 
.resultRow { 
	width: 110px; 
	margin: 10px 25px; 
	float: left; 
}

Let's start off small. We're going to be assigning each 'cell' of the results the class of resultRow. Since we want it presented neatly, we're going to be floating everything to the left so it forms a neat three column stack of results. This section is supposed to look like so after we've finished:


 
.correct, .wrong {     
	height: 19px; 
	cursor: pointer;  
   font-size: 16px; 
   padding: 5px 15px; 
  color: #4c4c4c; 
  border-radius: 4px; 
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); 
  -webkit-box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
  -moz-box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
}

Let's define the base of these blocks. As always, I'm using a little CSS3 to make the sections stand out. Other than that, the CSS is pretty simple -- a little padding, rounded corners and such.

 
.correct {     
  background: #b2d840; 
  background: -webkit-gradient(linear, left top, left bottom, from(#b2d840), to(#90b61e)); 
  background: -webkit-linear-gradient(#b2d840, #90b61e); 
  background-image: -moz-linear-gradient(top, #b2d840, #90b61e); 
  background-image: -moz-gradient(top, #b2d840, #90b61e); 
  border: 1px solid #5d8300; 
} 
.wrong { 
	background: #e84545; 
  background: -webkit-gradient(linear, left top, left bottom, from(#e84545), to(#c62323)); 
  background: -webkit-linear-gradient(#e84545, #c62323); 
  background-image: -moz-linear-gradient(top, #e84545, #c62323); 
  background-image: -moz-gradient(top, #e84545, #c62323); 
  border: 1px solid #930000; 
  color: #F1F1F1; 
}

We'll need to make the right and wrong answers stand out visually so here goes. Using CSS3 we're applying a green tinged gradient to all the right answers while the wrong 'uns get the red treatment. Nothing special going on here. Master the syntax and you should be all set.

 
.correct span { 
	padding: 0 20px; 
	background: url(img/confirm.png) left no-repeat; 
} 
.wrong span { 
	padding: 0 20px; 
	background: url(img/delete.png) left no-repeat; 
}

Let's add a little imagery to make the entire thing look more cohesive. We're adding some simple icons inside the buttons as shown above.

 
#answer-key { 
	text-align: center; 
	width: 300px; 
	padding: 15px; 
	margin: 0 auto; 
	 clear: both; 
	 font-size: 16px; 
}

And finally, let's style the answer key container. A little padding and margins help us make it look better. We're also making sure it is centered and setting the font size to an appropriate value.


The Final CSS

I skipped a few generic portions of the CSS along the way so here's the complete CSS at this point:

 
#main-quiz-holder { 
	margin: 0 auto; 
	position: relative;background: #FCFCFC; 
    border:1px solid #dedede; 
	 box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -o-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -webkit-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -moz-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 border-radius: 2px; 
	 position: relative; 
	 width: 600px; 
	 font-family:  "Myriad", "Myriad Pro", "Helvetica","Segoe UI", "Lucida Sans Unicode", "Lucida Grande", sans-serif; 
} 
#main-quiz-holder a { 
	text-decoration: none; 
} 
#results-container, #intro-container { 
	width: 500px; 
	text-align: center; 
} 
.questionContainer { 
    width: 560px; 
	 min-height: 400px; 
    padding: 20px; 
	 overflow: auto; 
	 margin: auto; 
} 
.questionContainer .question, h2.qTitle { 
    margin: 10px 0 20px 0; 
	 font-size: 26px; 
	 font-weight: normal; 
} 
h2.qTitle { 
	font-size: 32px; 
	margin-top: 30px; 
} 
.questionContainer ul.answers { 
    margin: 0px; 
    padding: 5px; 
	 list-style: none; 
} 
.questionContainer ul.answers li { 
	padding: 5px 50px; 
	margin: 12px 0; 
	color: #4c4c4c; 
  -webkit-border-radius: 4px; 
  -moz-border-radius: 4px; 
  border-radius: 4px; 
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); 
  -webkit-box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
  -moz-box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
  background: #f6f6f6; 
  background: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#d4d4d4)); 
  background: -webkit-linear-gradient(#f6f6f6, #d4d4d4); 
  background-image: -moz-linear-gradient(top, #f6f6f6, #d4d4d4); 
  background-image: -moz-gradient(top, #f6f6f6, #d4d4d4); 
  border: 1px solid #a1a1a1;	 
} 
.questionContainer ul.answers li.selected { 
	background: #6fb2e5; 
  box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -o-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -webkit-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -moz-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
} 
.questionContainer .prev, .questionContainer .next { 
	height: 19px; cursor: pointer; padding: 5px 10px; 
   font-size: 16px; 
   padding: 5px 10px; 
  color: #4c4c4c; 
  border-radius: 4px; 
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); 
  background: #6fb2e5; 
  box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -o-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -webkit-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
   -moz-box-shadow: 0 1px 5px #0061aa, inset 0 10px 20px #b6f9ff; 
} 
.questionContainer .next  { 
  background: #77d125; 
  box-shadow: 0 1px 5px #3caa00, inset 0 10px 20px #c9ffb6; 
   -o-box-shadow: 0 1px 5px #3caa00, inset 0 10px 20px #c9ffb6; 
   -webkit-box-shadow: 0 1px 5px #3caa00, inset 0 10px 20px #c9ffb6; 
   -moz-box-shadow: 0 1px 5px #3caa00, inset 0 10px 20px #c9ffb6; 
} 
#progressKeeper { 
    width: 553px; 
    margin: 0px 12px; 
	 box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -o-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -webkit-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 -moz-box-shadow:0 1px 5px #D9D9D9,inset 0 10px 20px #F1F1F1; 
	 border-radius: 2px; 
	 border:1px solid #dedede; 
	 position: absolute; 
	 bottom: 10px; 
	 left: 10px; 
} 
 #progress { 
    width: 0; 
	 height: 20px; 
  color: #4c4c4c; 
  background: #f6f6f6; 
  background: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#d4d4d4)); 
  background: -webkit-linear-gradient(#f6f6f6, #d4d4d4); 
  background-image: -moz-linear-gradient(top, #f6f6f6, #d4d4d4); 
  background-image: -moz-gradient(top, #f6f6f6, #d4d4d4); 
 } 
#resultKeeper { 
    margin: 10px; 
	 text-align: center; 
	 overflow: auto; 
} 
#notice { 
	position: absolute; 
	bottom: 40px; 
	right: 20px; 
} 
.questionContainer .prev { float: left;} 
.questionContainer .next, .questionContainer.btnShowResult { float: right; } 
.questionContainer .clear { clear: both; } 
.hide { display: none; } 
.btnPrev { 
	padding-left: 24px; 
	background: url(img/back.png) left no-repeat; 
} 
.btnNext { 
	padding-right: 24px; 
	background: url(img/forward.png) right no-repeat; 
} 
.btnShowResult{ 
	padding-left: 24px; 
	background: url(img/confirm.png) left no-repeat; 
} 
.btnStart { 
	display: block; 
	margin: 40px auto 0 auto; 
} 
.btnContainer { 
	margin: 20px 0 30px 0; 
	padding: 5px; 
} 
.resultRow { 
	width: 110px; 
	margin: 10px 25px; 
	float: left; 
} 
.correct, .wrong {     
	height: 19px; cursor: pointer; padding: 5px 10px; 
   font-size: 16px; 
   padding: 5px 15px; 
  color: #4c4c4c; 
  border-radius: 4px; 
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); 
  -webkit-box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
  -moz-box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), inset 0 0 6px 0 rgba(255, 255, 255, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4); 
} 
.correct {     
  background: #b2d840; 
  background: -webkit-gradient(linear, left top, left bottom, from(#b2d840), to(#90b61e)); 
  background: -webkit-linear-gradient(#b2d840, #90b61e); 
  background-image: -moz-linear-gradient(top, #b2d840, #90b61e); 
  background-image: -moz-gradient(top, #b2d840, #90b61e); 
  border: 1px solid #5d8300; 
} 
.wrong { 
	background: #e84545; 
  background: -webkit-gradient(linear, left top, left bottom, from(#e84545), to(#c62323)); 
  background: -webkit-linear-gradient(#e84545, #c62323); 
  background-image: -moz-linear-gradient(top, #e84545, #c62323); 
  background-image: -moz-gradient(top, #e84545, #c62323); 
  border: 1px solid #930000; 
  color: #F1F1F1; 
} 
.correct span { 
	padding: 0 20px; 
	background: url(img/confirm.png) left no-repeat; 
} 
.wrong span { 
	padding: 0 20px; 
	background: url(img/delete.png) left no-repeat; 
} 
#answer-key { 
	text-align: center; 
	width: 300px; 
	padding: 15px; 
	margin: 0 auto; 
	 clear: both; 
	 font-size: 16px; 
}

With everything but the JavaScript in place, our engine should look as shown below:



The JavaScript Interaction

The JavaScript, though shorter than you'd expect, is the heart and soul of the mini-engine. As I mentioned earlier, this is some pretty alpha code so make sure to clean things up before deployment.

As always, let's tackle the code one section at a time.


First, the Basics

We're going to handle some of the boilerplate elements first up.

 
$(function(){ 
// Everything that follows goes in here 
})

Let's place all our code into the block above -- a self executing anonymous function. This way our variables will be nicely namespaced thus avoiding naming collisions in the future.

 
var progress = $('#progress'), 
	progressKeeper = $('#progressKeeper'), 
	notice = $("#notice"), 
	progressWidth = 548, 
	answers= kroggy.answers, 
	userAnswers = [], 
	questionLength= answers.length, 
	questionsStatus = $("#questionNumber") 
	questionsList = $(".question");

And this is the variables we'll be using today. The names should make their purpose completely obvious. We also cache a few important elements for use later. An important point to make note of is how we're assigning answers the value of kroggy.answers. I'll explain it right below.

 
var kroggy = { answers: [ 'b', 'd', 'a', 'c', 'a', 'd', 'b', 'a', 'd', 'a', 'd', 'c', 'a', 'b', 'd' ] }

If you looked through the demo, you'll notice a small script tag holding the above code. What we're doing here is create an object called kroggy and place the answer key to our quiz inside an array called answers. This is the value we assigned to the variable answers earlier.


Creating the Helper Functions

Before we hop off into creating the actual code, let's deal with a few small helper methods first. In increasing order of importance.

 
function roundReloaded(num, dec) { 
	var result = Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); 
	return result; 
}

I couldn't find a decent enough rounding method predefined in JavaScript so I went ahead and put together a little something.

You'll need to send in a value and the number of significant digits to round off too and the function does the rest. Since the logic is incredibly simple, I'll let you parse the rest.

 
function judgeSkills(score) { 
	var returnString; 
		if (score==100) returnString = "Albus, is that you?" 
		else if (score>90) returnString = "Outstanding, noble sir!" 
		else if (score>70) returnString = "Exceeds expectations!" 
		else if (score>50) returnString = "Acceptable. For a muggle." 
		else if (score>35) returnString = "Well, that was poor." 
		else if (score>20) returnString = "Dreadful!" 
		else returnString = "For shame, troll!" 
	return returnString; 
}

The comments on the result screen netted a lot of hilarious comments and amusement among the quiz takers and this is where the magic happens.

Again, pretty basic programming that's going on here. The function takes the score as the parameter and returns a comment.

Here, the conditions are arbitrary but I think they do a fairly good job. Feel free to play around with the conditions in your project.

 
function checkAnswers() { 
	var resultArr = [],  
				flag = false; 
	for (i=0; i<answers.length; i++) { 
	    if (answers[i] == userAnswers[i]) { 
	        flag = true; 
	    } 
	    else { 
	        flag = false; 
	    } 
	    resultArr.push(flag); 
	} 
	return resultArr; 
}

This is the primary function that evaluates the checking process. Basically, we compare an array with another array and push the boolean result to another array. Simple as that.

In the code above, answers and userAnswers are the global variables we declared earlier. While the former is initialized and assigned a value early on, the latter won't be modified until the last minute i.e. when the final quiz question has been answered.

We merely loop through the array and for each element, we check the expected answer against the user's answer. If everything checks out, push a value of true to our result array. Else, push false.

When the evaluation has been completed, we return the result array so that it can be analyzed later.


Hooking up the Event Handlers

 
$('.btnStart').click(function(){ 
    $(this).parents('.questionContainer').fadeOut(500, function(){ 
        $(this).next().fadeIn(500, function(){ progressKeeper.show(); }); 
    }); 
		 return false; 
});

First, let's do the initial intro slide. Remember that all we're doing is display an image that is wrapped by an anchor? That's the anchor we're hooking the above event upto.

Essentially, we're hiding the parent of the clicked link element and then fading in the immediate sibling of the parent.

If you noticed that we aren't adding in each of these lines separately, good. You're paying attention. In the method above, we're passing the next step of the process as the callback to the animation function. This way, each of these animations happens one after the other instead of simultaneously which is the default behavior. This lets us create a smooth interface effect.

 
$('.btnPrev').click(function(){ 
		notice.hide(); 
    $(this).parents('.questionContainer').fadeOut(500, function(){ 
        $(this).prev().fadeIn(500) 
    }); 
    progress.animate({ width: progress.width() - Math.round(progressWidth/questionLength), }, 500 ); 
		 return false; 
});

The above code handles the previous button. The slider functionality is pretty similar. Hide the parent container and fade in the next element in the tree. Basically the same code as in the previous section but with the direction reversed.

With that done, we're tackling the progress bar next. What we're doing here is merely animating the width of the progress bar. No big calculating here -- divide the width of the progress bar by the number of question.

Since this is the previous button, we'll need to subtract the fraction from the total current width of the bar which is what we're doing here. May look a little complicated but trust me, it's fairly simple.

 
$('.btnNext').click(function(){ 
		var tempCheck = $(this).parents('.questionContainer').find('input[type=radio]:checked'); 
    if (tempCheck.length == 0) { 
         notice.fadeIn(300);return false; 
    } 
		 notice.hide(); 
    $(this).parents('.questionContainer').fadeOut(500, function(){ 
        $(this).next().fadeIn(500); 
    }); 
    progress.animate({ width: progress.width() + Math.round(progressWidth/questionLength), }, 500 ); 
		 return false; 
});

We have a few more things going on here so pay attention.

First up, we're creating a quick variable and assigning it an array of all radio buttons that have been checked. The input[type=radio]:checked selector helps us do this with minimal fuss.

We can now check the length of this array. If it's zero it means that the user hasn't selected an option and so we can show an error notice chiding the user for this. Which is what is happening inside the if statement. We immediately exit out of the function by using return false.

If we're past the previous step, it's time to proceed. We can now hide the notice, if it has been displayed before.

The slider logic again comes into play -- hide the current container and fade into the next container. We've seen it a number of time already and I don't think I need to rehash it.

Finally, we're handling the progress bar. Similar to the last block, we're calculating the additional width and then add it to the current width of the progress bar. Keep in mind that in the previous block, we subtracted it from the current width. In this block, we're adding it because we're moving to the next question and thus the progress bar forges ahead.


Gathering the User's Answers

 
$('.btnShowResult').click(function(){ 
// Stuff goes in here 
});

Ahh, the prodigal event handler that's at the heart of everything. We'll split the process into two parts:

  • Gathering data
  • Evaluating and displaying the results

We're going to tackle the first in this section. Keep in mind that all the JavaScript below goes in the handler above.

 
var tempCheck = $(this).parents('.questionContainer').find('input[type=radio]:checked'); 
if (tempCheck.length == 0) { 
     notice.fadeIn(300);return false; 
} 
var tempArr = $('input[type=radio]:checked'); 
for (var i = 0, ii = tempArr.length; i < ii; i++) { 
    userAnswers.push(tempArr[i].getAttribute('data-key')); 
}

The first few lines should appear very similar. It's because those lines are borrowed directly from the next button's handler. We're checking whether an option has been selected and if not, displaying an error message.

Once we've checked for monkey business, we basically creating an array of all checked checkboxes. We're then looping through the array and capturing the data-key attribute of each element. Remember these? We added these to the radio buttons during the HTML phase and point to the position of the checkbox from an alphabetic perspective. That is, the first option is a, second is b and so on.

What we're doing here is simply gathering the selected answers. Finding out the selected checkbox using the index method is an easier way, in retrospect but this is the way I used in the original codebase and that's what I'm sticking with today.

We push these values to the userAnswers global [within our scope] variable to be evaluated later.


Evaluating the Results

Now that we've gathered our data, we can now quickly analyze and display the results. Let's handle this in smaller chunks.

 
progressKeeper.hide(); 
var results = checkAnswers(),  
	 		  resultSet = '', 
			  trueCount = 0, 
			  answerKey = ' Answers <br />', 
			  score;

Since we're at the business end of things, we're hiding the progress bar. No need for it now.

We're also creating a bunch of variables to help us keep track of things internally. The variable results holds the results array that checkAnswers returns. We'll use it to render the final screen. The purpose of the rest of 'em should be apparent shortly.

 
for (var i = 0, ii = results.length; i &lt; ii; i++){ 
	if (results[i] == true) trueCount++; 
	resultSet += '<div class="resultRow"> Question #' + (i + 1) + (results[i]== true ? "<div class='correct'><span>Correct</span></div>": "<div class='wrong'><span>Wrong</span></div>") + "</div>"; 
	answerKey += (i+1) +" : "+ answers[i] +' &nbsp;  &nbsp;  &nbsp;   '; 
} 
score =  roundReloaded(trueCount / questionLength*100, 2);

Now that we have the data, let's do some nifty stuff with it. First, we loop through the results to find out the number of questions that the user has gotten right. We'll use this number to calculate the score later.

Next, we'll take it upon ourselves to render the spiffy looking results. Since we're already in the loop, we're going to use it to render everything else. We display the question number and depending on whether the user got that particular question right, place different HTML inside it. Remember, the CSS for the correct and wrong classes? They're being used here.

Since we'll need to display the answer key, we're using the same loop to step through the answers array and display the correct answer.

And finally, we're getting the rounded up score using the roundReloaded function we created earlier and storing the value in the score variable.


Displaying the Results

The meat of our work is done. We'll just need to wrap a few things up and display the results.

 
answerKey = "<div id='answer-key'>" + answerKey + "</div>"; 
resultSet = '<h2 class="qTitle">' +judgeSkills(score) + ' You scored '+score+'%</h2>' + resultSet + answerKey; 
$('#resultKeeper').html(resultSet).show(); 
	 $(this).parents('.questionContainer').fadeOut(500, function(){ 
    $(this).next().fadeIn(500); 
}); 
return false;

Very simple things going on here. First up, we wrap the value of answerKey with a div with an ID of answer-key to style it better.

Next up, we create the title of the result screen. We use the judgeSkills method to create an appropriate comments followed by some boilerplate to mention the score. We prepend these values to resultSet and to the final string add the answerKey

This concludes the final HTML that needs to be placed inside the results screen. We merely replace the HTML using our pre-generated HTML. With everything now in place, we fade the penultimate slide out and fade in the result screen.


Final Bits of Housekeeping

We have a few simple bits of housekeeping that we'll need to wrap things up.

 
progressKeeper.hide(); 
notice.hide(); 
$("#main-quiz-holder input:radio").attr("checked", false);

During initialization, on the splash screen, we have no need for the progress bar or the error message. Let's go ahead and hide it initially. Also, Firefox, for some reason, tends to 'remember' answers so let's erase that completely -- each checkbox is unselected when a page is refreshed.

 
$('.answers li input').click(function() { 
	$(this).parents('.answers').children('li').removeClass("selected"); 
	$(this).parents('li').addClass('selected'); 
});

Here's a little something to light up the selected option. Pay a little attention because things are a little dicey here.

When a checkbox is selected, we're traversing to the parent list element and removing the selected class from all its child li elements. After that, we're giving the clicked check box's parent a selected class so it shines up bright and blue.

And finally, to complete the slider functionality you'll need to hide all but the first slide. You can use JavaScript to do this or the easier CSS way -- add a display:none declaration to all but the first slide. That's exactly what the hide class does in the demo.


What We've Built





Wrapping up and a Friendly Plug

Phew! That was quite long, wasn't it? I see a sleezy joke there but I'll refrain!

If you enjoyed the quiz engine that we built today, I built an even more advanced version. One with answer reviews on the result page, social sharing, question counters and much more. It's so good that even the Tuts+ sites are going to use it!

Have a quick look at jQuizzy, my new quiz engine. You'll love it, I promise!

Anywho, we're finished here. We looked at creating a quiz engine, from scratch, covering all aspects from the HTML to the CSS to the JavaScript. We looked at nifty techniques to style elements and to program interaction through JavaScript. Hopefully, you found this interesting and useful.

If you run into any issues, leave me a comment. Thank you so much for reading!

Advertisement