Skip to content

Instantly share code, notes, and snippets.

@shiftyp
Last active September 30, 2022 10:26
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save shiftyp/0e2516f91a044acfb396 to your computer and use it in GitHub Desktop.
Save shiftyp/0e2516f91a044acfb396 to your computer and use it in GitHub Desktop.
OOP Quiz App

OOP and MVC

What and Why

One of the big leaps you'll have to make as a JavaScript developer is wrapping your head around Object Oriented Programming (OOP). This is tough, and that's ok because it's tough for everyone at first. When you start out with JavaScript you're taught to use functions as your primary way of organizing your code. This is fine, but you'll probably find that organizing your code around objects makes larger projects easier to accomplish and improve / maintain.

The cool thing is that what OOP amounts to is an organizational strategy. I have a set of related tasks, how do I go about starting the project and organizing my code? These tasks have some variables and functions that are used to accomplish them, so you create them and write the logic for them to interact. While you can write those out as detached functions and variables, making those variables and functions into properties and methods of an object can make the division between those tasks easier to see and maintain.

Maintaining some separation between your tasks is important, because it means bugs will be restricted to one place in the code. This makes them easier to fix. It's also clear where to add features, because you pick an existing object that is related to it or make a new one and that task is now neatly in one place. Dividing your tasks into discrete units also lets you work on them one at a time and somewhat independently, which solves the question of "Where do I start?" You start with the objects and tasks that the most other objects and tasks are dependent on and go from there.

The Quiz Problem

Let's take a real world example. You're working as a developer at ACME Software Industries, and your boss comes to you and says "Our clients want to be quizzed on things, make a quiz app!". Generally your boss sees the big picture, and it's up to you take that large problem and divide it into smaller tasks, and eventually into lines of code that solve the overall problem. So let's think about the smaller problems involved in creating this application:

  • We'll need some way to store the questions in the quiz along with their answers.
  • We'll need to write some logic that moves from one question to another and starts and restarts the quiz.
  • We'll need to do some DOM interaction like handling a click on a button and displaying a question to the user.

Within those tasks there are a lot of smaller tasks, and there are a lot of ways you could go about writing them. I chose to divide the tasks into these three categories for a reason though, and that reason is called MVC.

Model View Controller (MVC)

Just saying "use objects" is pretty vague, and the natural follow up question is "Well, which objects should I use?" There are lots of different strategies depending on the particular problem you're trying to solve, but the main idea is to lump your tasks into some categories that don't have much to do with one another and create objects within those categories. MVC is a set of categories for objects that fit a wide variety of tasks. Here's how it breaks down in this case.

  1. Model: Our quiz app is a data driven application. The data in this case are our questions. So we take our questions and the functions that interact with the data for those questions and put them into a set of objects.
  2. View: We're taking those questions and displaying them to the user, and handling user input. We can make a set of objects that just handle those tasks.
  3. Controller: Once we've separated out the data part and the view part, what's left is the logic that coordinates those two to create the application. We take that logic and put it into it's own object or objects as well.

The rationale behind MVC is really best explained by a question: "What things in my app are likely to change independently of eachother?" This is a good question to ask, because we don't want to mix these tasks and make them dependent on eachother. Otherwise every time we make a change in one we'll have to make some changes in the others. This increases the cost in terms of how many changes you'll have to make to add a feature or fix a bug, and our boss doesn't like cost. We don't like cost either, because it means work and frustration.

If you change the class on a button, does that affect how you check the answer to a question? If you add a feature to restart the quiz, does that change the way questions are displayed? The answers to these might end up being yes, but in many applications the concerns for your data, presentation, and logic can be separated in a way that they don't affect one another to a great extent. So for this application dividing up our tasks into objects that deal with data, objects that deal with presentation, and objects that deal with logic makes a lot of sense.

So are you ready to look at some code? Let us begin...

!function() {
/*
* Data:
*
* Here are some quiz questions.
*/
var data = [
{
prompt: 'What is not a principle of Object Oriented Programming',
answers: [
'Abstraction',
'Encapsulation',
'Inheritence',
'Polymorphism',
'Impressionism'
],
correctIndex: 4
},{
prompt: 'What type of inheritence pattern is utilized in JavaScript?',
answers: [
'Prototypal',
'Classical',
'Trust'
],
correctIndex: 0
},{
prompt: 'Which is better? Functional Programming or Object Oriented Programming?',
answers: [
'Object Oriented Programming',
'Functional Programming',
'Neither, everything has its uses'
],
correctIndex: 2
}
];
/*
* Handling Data (Model):
*
* These objects handle all of the data for the app. In this
* case our data are in the form of quiz questions, where each
* question has a prompt, a set of answers, and a correct answer.
*/
/*
* The Question object represents a single question. It has
* properties that reflect the data, and a set of methods
* to interact with that data.
*/
function Question(datum) {
this.prompt = datum.prompt;
this.answers = datum.answers;
this.correctIndex = datum.correctIndex;
}
Question.prototype.checkAnswer = function(index) {
return index === this.correctIndex;
};
Question.prototype.forEachAnswer = function(callback, context) {
this.answers.forEach(callback, context);
};
/*
* The Quiz object is a collection of question objects.
* It creates the questions from data, stores the questions,
* and keeps track of what question you're on and how many
* questions you've gotten right.
*/
function Quiz(data) {
this.numberCorrect = 0;
this.counter = 0;
this.questions = [];
this.addQuestions(data);
}
Quiz.prototype.addQuestions = function(data) {
for (var i = 0; i < data.length; i++) {
this.questions.push(new Question(data[i]));
}
};
Quiz.prototype.advanceQuestion = function(lastAnswer) {
if (this.currentQuestion && this.currentQuestion.checkAnswer(lastAnswer)) {
this.numberCorrect++;
}
this.currentQuestion = this.questions[this.counter++];
return this.currentQuestion;
};
/*
* Handling Logic (Controller)
*
* These objects handle the business logic of our app. The logic
* in this case is "start quiz", "next question" and "end quiz".
*/
/*
* The QuizApp object coordinates all the other objects in the
* application, and controls the flow of the quiz.
*/
function QuizApp(data) {
this.data = data;
this.introView = new IntroView('#quiz-intro', this);
this.outroView = new OutroView('#quiz-outro', this);
this.questionView = new QuestionView('#quiz-form', this);
this.introView.attachEventHandlers();
this.outroView.attachEventHandlers();
this.questionView.attachEventHandlers();
}
QuizApp.prototype.startQuiz = function() {
this.quiz = new Quiz(this.data);
this.introView.toggle(true);
this.outroView.toggle(true);
this.questionView.toggle(false);
this.nextQuestion();
};
QuizApp.prototype.nextQuestion = function(answer) {
var nextQuestion = this.quiz.advanceQuestion(answer);
if (nextQuestion) {
this.questionView.setQuestion(nextQuestion);
} else {
this.endQuiz();
}
};
QuizApp.prototype.endQuiz = function() {
this.questionView.toggle(true);
this.outroView.toggle(false);
this.outroView.displayOutroMessage(this.quiz.numberCorrect, this.quiz.questions.length);
};
/*
* Handling Presentation (View):
*
* These objects handle all of the manipulation of the DOM as well
* as handling events triggered on the DOM. We have three views, one
* for each section of the application.
*/
/*
* The IntroView handles interaction with the #quiz-intro section
* and its .start-button. When the start button is clicked it
* starts the quiz by interacting with its QuizApp object through
* the startQuiz method. It also implements methods to attach
* event handlers and toggle its visibility.
*/
function IntroView(selector, quizApp) {
this.element = $(selector);
this.startButton = this.element.find('.start-button');
this.quizApp = quizApp;
}
IntroView.prototype.attachEventHandlers = function() {
var self = this;
this.startButton.click(function() {
self.quizApp.startQuiz();
});
};
IntroView.prototype.toggle = function(hide) {
this.element.toggleClass('hidden', hide);
};
/*
* The OutroView is similar to the IntroView, with the addition
* of a displayOutroMessage method which displays an appropriate
* message based on the number of correct answers and the total
* number of questions.
*/
function OutroView(selector, quizApp) {
this.element = $(selector);
this.resetButton = this.element.find('.reset-button');
this.outroMessage = this.element.find('.quiz-outro-message');
this.quizApp = quizApp;
}
OutroView.prototype.displayOutroMessage = function(numberCorrect, totalQuestions) {
var message = 'You got ' + numberCorrect + ' questions right out of ' +
totalQuestions + '. Would you like to try again?';
this.outroMessage.html(message);
};
OutroView.prototype.attachEventHandlers = function() {
var self = this;
this.resetButton.click(function() {
self.quizApp.startQuiz();
});
};
OutroView.prototype.toggle = function(hide) {
this.element.toggleClass('hidden', hide);
};
/*
* The QuestionView is where most of the action is. It has similar methods
* that attach event handlers and toggle the element visibility. It also
* implements a setQuestion method that takes a question and generates
* the HTML for the prompt and answers and puts them into the DOM.
*/
function QuestionView(selector, quizApp) {
this.element = $(selector);
this.submitAnswerButton = this.element.find('.submit-answer-button');
this.questionContainer = this.element.find('.question-container');
this.answersContainer = this.element.find('.answers-container');
this.quizApp = quizApp;
}
QuestionView.prototype.attachEventHandlers = function() {
var self = this;
this.submitAnswerButton.click(function() {
var checkedInput = self.answersContainer.find('input:checked');
if (!checkedInput.length) alert('Please select an answer');
else {
var answer = +checkedInput.val();
self.quizApp.nextQuestion(answer);
}
});
};
QuestionView.prototype.setQuestion = function(question) {
var radios = '';
this.questionContainer.text(question.prompt);
question.forEachAnswer(function(answer, index) {
radios +=
'<li>' +
'<input type="radio" name="answer" value="' + index + '" id="answer' + index + '"></input>' +
'<label for="answer' + index + '">' + answer + '</label>' +
'</li>';
});
this.answersContainer.html(radios);
};
QuestionView.prototype.toggle = function(hide) {
this.element.toggleClass('hidden', hide);
};
/*
* Then when the document is ready, we do stuff!!!
*/
$(function() {
var quizApp = new QuizApp(data);
});
}();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OOP Quiz App</title>
<link href="styles.css" rel="stylesheet">
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.js" type="text/javascript"></script>
<script src="app.js" type="text/javascript"></script>
</head>
<body>
<h1>Object Oriented Programming Quiz</h1>
<div class="content">
<div class="content-inner">
<!--
#quiz-intro shown by default when the page loads, and contains
a challenge to the quiz taker and a button to begin the quiz
-->
<div id="quiz-intro" class="text-center">
<h2>You down with OOP? Well try and see...</h2>
<button class="start-button" type="button">Start Quiz</button>
</div>
<!--
#quiz-form will contain the question and answers in the
.question-container and .answer-container respectively.
It also contains a button to submit an answer.
-->
<form id="quiz-form" class="hidden">
<div class="quiz-container">
<p class="question-container text-center"></p>
<ul class="answers-container">
</ul>
</div>
<div class="text-center">
<button class="submit-answer-button" type="button">Submit Answer</button>
</div>
</form>
<!--
#quiz-outro is shown when the user completes the quiz. An
appropriate message is displayed in .quiz-outro-message,
and the quiz taker can reset the quiz using the reset button.
-->
<div id="quiz-outro" class="hidden text-center">
<div class="quiz-outro-message"></div>
<button class="reset-button" type="button">Try again?</button>
</div>
</div>
</div>
</body>
</html>
body {
margin: 0;
background-image: url('congruent_pentagon.png');
font-family: sans-serif;
font-size: 20px;
color: rgb(121, 127, 124);
}
h1 {
text-align: center;
}
h2 {
margin-top: 0;
}
div, form {
box-sizing: border-box;
}
button {
background-color: rgb(0, 81, 115);
color: #fff;
padding: 0.5em;
border: none;
border-radius: 5px;
font-size: 15px;
line-height: 1.5em;
margin-top: 20px;
}
.hidden {
display: none;
}
.text-center {
text-align: center;
}
.content {
position: relative;
width: 800px;
padding: 20px;
margin: 50px auto;
background-color: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.4);
border-bottom-width: 10px;
border-radius: 5px;
border-bottom-radius: 10px;
overflow: hidden;
}
/*
* In case you're wondering how the content seems
* to blur the background behind it this is the trick
*/
.content::before {
position: absolute;
top: -20px;
left: -20px;
content: ' ';
width: 100%;
height: 100%;
padding: 20px;
background-image: url('congruent_pentagon.png');
background-position: center 100px;
-webkit-filter: blur(10px);
z-index: -1;
}
@media (max-width: 800px) {
.content {
width: 100%;
}
}
.answers-container {
list-style: none;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment