Create a Data Driven Quiz App in Ionic 2 & 3 – Part 2

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:

Ionic 2 Quiz App

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:

Ionic 2 Quiz App

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.

Advanced Animations & Interactions with Ionic

NEW Create next level Ionic applications by harnessing the power of the Ionic Animations API and Gestures.

Utilise these powerful APIs to design and build your own custom animations and interactions. No external libraries required, everything is built with just Ionic. Learn more.

Follow me on Twitter or subscribe to me on YouTube for more web development content.

Check out my latest videos: