In Part 1 of this tutorial series we started building a data driven quiz application for kids using Ionic 2. We have been making use of a Flash Card Component that was created in a previous tutorial to display the answers to questions in a fun and interactive way.
The end product of this tutorial will look like this:
But as of right now, we only have a single question hard coded into the application and there is absolutely no styling. In this tutorial, we are going to finish off the application by loading in some real data, adding the logic required for navigating through the quiz, and adding some styling.
Before We Get Started
Last updated for Ionic 3.9.2
Before you begin this tutorial, you must have completed the previous tutorial. This is going to pick up right where the last tutorial left off.
1. Add the Raw JSON Data
The first thing we are going to focus on doing is adding some real data to our application. We are going to use a JSON file that is stored locally to pull the data in from. This JSON data could just as easily be stored somewhere else though. We will be making a HTTP request to load the local JSON file, but you could instead make a HTTP request to a server somewhere if you prefer.
Create a file called questions.json at src/assets/data/questions.json (you will need to create the data folder)
Add the following to the questions.json file:
{
"questions": [
{
"flashCardFront": "<img src='assets/images/helicopter.png' />",
"flashCardBack": "Helicopter",
"flashCardFlipped": false,
"questionText": "What is this?",
"answers": [
{"answer": "Helicopter", "correct": true, "selected": false},
{"answer": "Plane", "correct": false, "selected": false},
{"answer": "Truck", "correct": false, "selected": false}
]
},
{
"flashCardFront": "<img src='assets/images/plane.png' />",
"flashCardBack": "Plane",
"flashCardFlipped": false,
"questionText": "What is this?",
"answers": [
{"answer": "Helicopter", "correct": false, "selected": false},
{"answer": "Plane", "correct": true, "selected": false},
{"answer": "Truck", "correct": false, "selected": false}
]
},
{
"flashCardFront": "<img src='assets/images/truck.png' />",
"flashCardBack": "Truck",
"flashCardFlipped": false,
"questionText": "What is this?",
"answers": [
{"answer": "Helicopter", "correct": false, "selected": false},
{"answer": "Plane", "correct": false, "selected": false},
{"answer": "Truck", "correct": true, "selected": false}
]
}
]
}
This defines all of the data that we will need for our application. We use JSON to define an array of questions
, which contains three objects
that define the data for each question.
Each question has some text that will be displayed, and an array of possible answers to that question. The correct answer is identified by setting the correct
property to true, and we also have a selected
property that we will make use of later (this will keep track of which answer the user selected).
We also have some configuration for the flash cards. We define the front and back content for the card, and we also store a flashCardFlipped
property that will be responsible for controlling whether or not the card should be flipped.
2. Set up The Data Provider
Now that have our data set up, we need to pull it into the application. We are going to implement our Data
provider that we created in the previous tutorial to handle loading in this data. Even though the data is stored in a local file, we still need to make a HTTP request to load the data.
Modify src/providers/data.ts to reflect the following:
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class Data {
data: any;
constructor(public http: Http) {
}
load(){
if(this.data){
return Promise.resolve(this.data);
}
return new Promise(resolve => {
this.http.get('assets/data/questions.json').map(res => res.json()).subscribe(data => {
this.data = data.questions;
resolve(this.data);
});
});
}
}
This is a very standard looking data provider. We have a load
function that will handle loading in the data. If the data has already been fetched then it just returns a promise that immediately resolves with the data. If the data has not already been loaded, then a HTTP request is made to the local JSON file, and we convert the JSON response into an object we can work with by using the map
operator.
If you would like a more in-depth explanation as to how mapping works, take a look at: How to Manipulate Data in Ionic 2.
Now we will be able to use this provider to access the question data wherever we need it.
3. Add the Quiz Logic
This is the fun bit. We’re going to finish implementing the logic for the quiz. There’s a few things we are going to add now that we didn’t do in the last tutorial, which is:
- Fetching the question data
- Randomising the answers (so that they don’t always appear in the same order)
- Adding logic to handle the selection of answers, and proceeding to the next question
- Keep track of scoring
- The ability to restart the quiz
Let’s implement the code first and then talk through it.
Modify src/pages/home/home.ts to reflect the following:
import { Component, ViewChild } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Data } from '../../providers/data';
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
@ViewChild('slides') slides: any;
hasAnswered: boolean = false;
score: number = 0;
slideOptions: any;
questions: any;
constructor(public navCtrl: NavController, public dataService: Data) {
}
ionViewDidLoad() {
this.slides.lockSwipes(true);
this.dataService.load().then((data) => {
data.map((question) => {
let originalOrder = question.answers;
question.answers = this.randomizeAnswers(originalOrder);
return question;
});
this.questions = data;
});
}
nextSlide(){
this.slides.lockSwipes(false);
this.slides.slideNext();
this.slides.lockSwipes(true);
}
selectAnswer(answer, question){
this.hasAnswered = true;
answer.selected = true;
question.flashCardFlipped = true;
if(answer.correct){
this.score++;
}
setTimeout(() => {
this.hasAnswered = false;
this.nextSlide();
answer.selected = false;
question.flashCardFlipped = false;
}, 3000);
}
randomizeAnswers(rawAnswers: any[]): any[] {
for (let i = rawAnswers.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = rawAnswers[i];
rawAnswers[i] = rawAnswers[j];
rawAnswers[j] = temp;
}
return rawAnswers;
}
restartQuiz() {
this.score = 0;
this.slides.lockSwipes(false);
this.slides.slideTo(1, 1000);
this.slides.lockSwipes(true);
}
}
First, let’s take a look at the new member variables we have added:
@ViewChild('slides') slides: any;
hasAnswered: boolean = false;
score: number = 0;
slideOptions: any;
questions: any;
The slides
and slideOptions
variables were both added in the last tutorial, but all the rest are new. The hasAnswered
variable will be used to keep track of when the user has selected an answer. If the user has selected an answer, then we want to block them from guessing again (until they get to the next question). The score
variable will keep track of the number of questions the user has guessed correctly, and questions
will be used to keep a reference to the questions data loaded in from the provider.
Also, note that we have got rid of the flashCardFlipped
variable – we were using that in the last tutorial just so that we could see the flash card flip animation, but now we will be setting it properly by using the flashCardFlipped
property that is stored on each question.
In the ionViewDidLoad()
function (which is automatically triggered when the page is loaded), we load in the data from the data provider, and then we use a map
operation. The map
operation allows us to change the values of the array based on some function, and in this case, we are using map
to randomise the order of the answers array contained in the question. To do this, we run the answers through the randomizeAnswers
function (which I grabbed from a StackOverflow question, but I’ve lost the link to provide credit, so thank you to whoever you are!).
Again, if you’d like to know more about how mapping works, take a look at: How to Manipulate Data in Ionic 2.
The selectAnswer
function takes in two parameters: answer (the answer the user selected) and question (the data for the entire question). This first sets the hasAnswered
variable so that we know to disable the input area, it sets the flashCardFlipped
property to true so that the answer is revealed, and if the answer is correct it increments the user’s score. It then waits for 3 seconds before resetting the values and proceeding to the next question.
Finally, we have the restartQuiz
function, which just resets the score and takes the user back to the first question.
4. Finish the Quiz Template
We’ve got all the logic sorted, now we just need to finish implementing the template for the quiz.
Modify src/pages/home/home.html to reflect the following:
<ion-content>
<ion-slides #slides>
<ion-slide class="start-slide">
<button ion-button color="primary" (click)="nextSlide()">Start!</button>
</ion-slide>
<ion-slide *ngFor="let question of questions; let i = index;">
<h3>Question {{i+1}}</h3>
<flash-card [isFlipped]="question.flashCardFlipped">
<div
class="flash-card-front"
[innerHTML]="question.flashCardFront"
></div>
<div class="flash-card-back" [innerHTML]="question.flashCardBack"></div>
</flash-card>
<h3>{{question.questionText}}</h3>
<ion-list no-lines radio-group>
<ion-item *ngFor="let answer of question.answers; let i = index;">
<ion-label>{{i+1}}. {{answer.answer}}</ion-label>
<ion-radio
(click)="selectAnswer(answer, question)"
[checked]="answer.selected"
[disabled]="hasAnswered"
></ion-radio>
</ion-item>
</ion-list>
</ion-slide>
<ion-slide>
<h2>Final Score: {{score}}</h2>
<button (click)="restartQuiz()" ion-button full color="primary">
Start Again
</button>
</ion-slide>
</ion-slides>
</ion-content>
There’s actually a few interesting things going on here, so let’s talk through them.
First, we are using the Slides component to display questions. We create a single slide at the start which will display a “Start” button, and we also add a slide manually at the end to display the users score (and let them reset the quiz). In between those two slides, we loop with *ngFor
to create a slide for every question that we have.
In the *ngFor
that we have set up, we add a second statement:
let i = index;
this will keep track of the index of the item we are up to when looping. For the first item i
will be , for the second it will be `1`, for the third it will be `2`, and so on. We create an index variable for both the question and the answers. This will allow us to display the question number, and also number each of the answers. Notice that we use `{{i+1}}` though, because the index starts at
.
The last interesting thing here is our use of <ion-radio>
. We added a (click)
handler to send through the answer and question to our selectAnswer
function when the user selects an answer, and then we set the checked
and disabled
properties. When disabled
is set to true, the user will no longer be able to interact with the <ion-radio>
inputs. When checked
is set to true, a tick will display next to the answer the user has selected – this is useful to us because it allows us to uncheck the answers the user has selected when they restart the quiz.
5. Styling
The application is just about done now, we are just going to add a few styles to make things look a little prettier.
Modify the
Shared Variables
in src/theme/variables.scss to reflect the following:
$text-color: #fff;
$background-color: #9b59b6;
Modify the
Named Color Variables
in src/theme/variables.scss to reflect the following:
$colors: (
primary: #b065cf,
secondary: #32db64,
danger: #f53d3d,
light: #f4f4f4,
dark: #222
);
Modify src/pages/home/home.scss to reflect the following:
.ios,
.md {
page-home {
ion-slide {
align-items: flex-start;
}
ion-item {
margin-bottom: 5px;
font-size: 1.2em;
background-color: map-get($colors, primary);
}
flash-card {
color: #000;
img {
width: 70%;
height: auto;
}
}
.start-slide {
justify-content: center;
align-items: center;
button {
font-size: 1.3em;
font-weight: bold;
}
}
}
}
There’s nothing too weird going on in the styles above, except for the align-items
property we set on ion-slide
. By default, slide components will vertically align content to the center. We want the content to begin from the top of the page so we set it to flex-start
.
If you run the application using ionic serve
now you should hopefully see the completed application:
Summary
The quiz we created in this tutorial series is quite basic, but the general structure is quite powerful. You would be able to extend this quite easily and use it for a more complicated quiz style application.
We’ve also covered some important lessons, including how to provide configuration values to custom components, how to load and manipulate data from a static JSON source, and how to navigate programatically between slides.