Create a Desktop Quiz Application Using Vue.js and Electron

Ogundipe Samuel Ayo

Today, I would be explaining how to build a desktop quiz application using Electron and Vue.js.

Vue.js is a library for building interactive web interfaces. It provides data-reactive components with a simple and flexible API.

The reason why I chose to use Vue is because it is a lightweight alternative to Angular, and it is very easy for any developer at an intermediate level to pick up.

Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application. It is based on Node.js and Chromium and is used by the Atom editor.

If you are familiar with technologies such as C# or Java, you know that building desktop apps can be a bit tedious, and sometimes you might have to write DLL (dynamic-link library) files yourself.

Electron, on the other hand, takes care of all of these for you, and may I mention, it is also cross platform. I.e one code base, all three platforms.

What We Will Build

We will be building a desktop application that allows people to take quiz questions, and at the end of the questions, see their total score.

Here is a quick view of what we will be building:

For the purpose of this tutorial, the term Vue refers to Vue 2.X versions unless stated otherwise.

This tutorial, however, hopes that you do understand the basics of Vue.js

Getting Started With The Vue-Cli

To get started easily and also skip the process of configuring Webpack for the compilation from ES2016 to ES15, we will use the Vue CLI. If you do not have the Vue CLI installed, we can install it by running the command below:

sudo npm install -g vue-cli

After installing the Vue-cli, we will proceed to create a Vue project. To do that, we run the following command.

Note: for the purposes of this tutorial, while running the command below, I choose no when asked if to lint the code.

Code linting will ensure that the code is indented and empty spaces are not left. But I like to leave empty spaces in my code to keep it organized.

vue init webpack electron-vue

Now we will need to install the NPM dependencies for the application to work.

We would have to change directory to the working folder and then run npm install.

//change directory into the folder
cd electron-vue
//install the npm dependencies
npm install

After installing these modules, we need to install Electron on our system.

Installing And Configuring Electron

I usually prefer running Electron globally, but you can install locally.

To install globally, we can run:

sudo npm install -g electron

To install locally, we can run:

npm install electron

At this point, if we run the electron command, we should get an error like this:

electron .
Error launching app
Unable to find Electron app at /home/samuel/electron-vue
Cannot find module '/home/samuel/electron-vue'

This error is because you have not created the main script that starts the Electron app. So let's move into our package.json file which has been created by our cue-cli and add a new line to it under the first object block, which defines name, version, description, author, private. Just before the scripts also, we would add a key pair called "main" with a value of "elect.js" .

{
  //name of the application
  "name": "electron-vue",
  //version of the application
  "version": "1.0.0",
  //description of the application
  "description": "A Vue.js project",
  //author of the application
  "author": "samuelayo <ayoogundipe2005@gmail.com>",
  "private": true,
  //main entry point for electron
  "main": "elect.js",
  //vue js defined scripts
  "scripts": {
  "dev": "node build/dev-server.js",
  "build": "node build/build.js"
  }

We have defined elect.js as the starting point for Electron to run its application, but the file hasn’t been created yet.

So in our root folder, we will create a file called elect.js and put in the following content:

// ./main.js
const {app, BrowserWindow} = require('electron')

let win = null;

app.on('ready', function () {

  // Initialize the window to our specified dimensions
  win = new BrowserWindow({width: 1000, height: 600});

  // Specify entry point to default entry point of vue.js
  win.loadURL('http://localhost:8080');

  // Remove window once app is closed
  win.on('closed', function () {
  win = null;
  });

});
//create the application window if the window variable is null
app.on('activate', () => {
  if (win === null) {
  createWindow()
  }
})
//quit the app once closed
app.on('window-all-closed', function () {
  if (process.platform != 'darwin') {
  app.quit();
  }
});

The above created file is the elect.js file which will now run the application. At the beginning of this file, we required Electron and set the values of two different constants to it, namely: app and browserwindow;

We then set the variable win to null by default. At this point, we put in three listeners to listen the activate, ready and windows-all-closed events.

The main focus here is the ready event, where we set a new dimension for our app, load the entry point, which could be a direct URL or a file URL. But for now, we are loading the development URL for Vue.

At this point, we can open up two terminals to the root of our project.

In the first terminal, we want to serve our Vue application, So we run:

npm run dev

On the second terminal, we want to run the Electron application, so we run:

electron .

If all goes well, we should be seeing this:

At this point, we are all set to go. All we need to do now is focus on our Vue application.

Creating The Quiz Application

It's time to move into our src/App.vue to do the main quiz application.

At this point, let's deal with the script part of our App.vue file. We'll replace the script file with the following:

<script>
// an array of questions to be asked. Length of 10 questions.
var quiz_questions = [
  {
  "category": "Entertainment: Film",
  "type": "multiple",
  "difficulty": "easy",
  "question": "Who directed "E.T. the Extra-Terrestrial" (1982)?",
  "correct_answer": "Steven Spielberg",
  "incorrect_answers": [
  "Steven Spielberg",
  "Stanley Kubrick",
  "James Cameron",
  "Tim Burton"
  ]
  },
  {
  "category": "Entertainment: Video Games",
  "type": "multiple",
  "difficulty": "medium",
  "question": "What is the main character of Metal Gear Solid 2?",
  "correct_answer": "Raiden",
  "incorrect_answers": [
  "Raiden",
  "Solidus Snake",
  "Big Boss",
  "Venom Snake"
  ]
  },
  {
  "category": "Science & Nature",
  "type": "multiple",
  "difficulty": "easy",
  "question": "What is the hottest planet in the Solar System?",
  "correct_answer": "Venus",
  "incorrect_answers": [
  "Venus",
  "Mars",
  "Mercury",
  "Jupiter"
  ]
  },
  {
  "category": "Entertainment: Books",
  "type": "multiple",
  "difficulty": "hard",
  "question": "What is Ron Weasley's middle name?",
  "correct_answer": "Bilius",
  "incorrect_answers": [
  "Bilius",
  "Arthur",
  "John",
  "Dominic"
  ]
  },
  {
  "category": "Politics",
  "type": "multiple",
  "difficulty": "medium",
  "question": "Before 2011, "True Capitalist Radio" was known by a different name. What was that name?",
  "correct_answer": "True Conservative Radio",
  "incorrect_answers": [
  "True Conservative Radio",
  "True Republican Radio",
  "Texan Capitalist Radio",
  "United Capitalists"
  ]
  },
  {
  "category": "Entertainment: Film",
  "type": "multiple",
  "difficulty": "medium",
  "question": "This movie contains the quote, "I love the smell of napalm in the morning!"",
  "correct_answer": "Apocalypse Now",
  "incorrect_answers": [
  "Apocalypse Now",
  "Platoon",
  "The Deer Hunter",
  "Full Metal Jacket"
  ]
  },
  {
  "category": "History",
  "type": "multiple",
  "difficulty": "medium",
  "question": "The Herero genocide was perpetrated in Africa by which of the following colonial nations?",
  "correct_answer": "Germany",
  "incorrect_answers": [
  "Germany",
  "Britain",
  "Belgium",
  "France"
  ]
  },
  {
  "category": "Entertainment: Music",
  "type": "boolean",
  "difficulty": "medium",
  "question": "Ashley Frangipane performs under the stage name Halsey.",
  "correct_answer": "True",
  "incorrect_answers": [
  "True",
  "False"
  ]
  },
  {
  "category": "Entertainment: Books",
  "type": "multiple",
  "difficulty": "easy",
  "question": "Under what pseudonym did Stephen King publish five novels between 1977 and 1984?",
  "correct_answer": "Richard Bachman",
  "incorrect_answers": [
  "Richard Bachman",
  "J. D. Robb",
  "Mark Twain",
  "Lewis Carroll"
  ]
  },
  {
  "category": "History",
  "type": "multiple",
  "difficulty": "medium",
  "question": "In what prison was Adolf Hitler held in 1924?",
  "correct_answer": "Landsberg Prison",
  "incorrect_answers": [
  "Landsberg Prison",
  "Spandau Prison",
  "Ebrach Abbey",
  "Hohenasperg"
  ]
  }
]
export default {
//name of the component
  name: 'app',
  //function that returns data to the components
  data : function (){
  return{
//question index, used to show the current question
  questionindex:0,
//set the variable quizez to the questions defined earlier
  quizez:quiz_questions,
//create an array of the length of the questions, and assign them to an empty value.
answers:Array(quiz_questions.length).fill(''),
  }
  },
  //methods to be called in the component
  methods: {
  // Go to next question
  next: function() {
  this.questionindex++;
  },
  // Go to previous question
  prev: function() {
  this.questionindex--;
  }
 },
 computed:{
 //calculate total score of the quiz person.
  score: function() {
  var total = 0;
  for (var i =0; i <this.answers.length; i++) {
  if(this.answers[i]==this.quizez[i].correct_answer){
  total +=1;
  }
  }
  return total;
  }
 }
}
</script>

In the above code, we declared a variable called quiz_questions and gave it an array of objects which are the intended questions. The objects had a key for category of the question, the type of the question, the difficulty level of the question, the question itself, the correct_answer, and an array of incorrect answers, which consist of all the options to all questions.

After this, we then go ahead to declare our component properties, such as the name, the data needed, our methods and a computed property.

The data block consist of the questionindex which refers to the current question being shown, the quizez which represent the questions available to be shown, which is set to the value of the quiz_questions, and another property called answers which is instantiated to an array with a length of the current amount of questions, and their values are all set to an empty string.

The methods block consist of two methods, namely: next and previous, which increases and decreases the value of the questionindex, which would be used when the person clicks previous and next.

The computed block, consists of one computed property, which checks all the answers given to the questions using their index, which will tally to the index of the quiz questions, then check if the values are equal to the correct_answer key of the question, and if correct, scores the person for each question.

At this point, we are ready to change the template section and have it display our quiz questions.

Let's replace the content of the <template></template> tag with this:

<template>
  <div id="app">
  <!-- Questions: display a div for each question -->
<!-- show only if the index of the quetion is equal to the question index -->
  <div v-for="(quiz, index) in quizez" v-show="index === questionindex">
<!-- display the quiz Category -->
  <h1>{{ quiz.category }}</h1>
<!-- display the quiz question -->
  <h2>{{ quiz.question }}</h2>
  <!-- Responses: display a li for each possible response with a radio button -->
  <ol>
<!--display the quiz options -->
  <li v-for="answer in quiz.incorrect_answers">
  <label>
<!-- bind the options to the array index of the answers array that matches this index -->
  <input type="radio" name="answer" v-model="answers[index]" :value="answer"> {{answer}}
  </label>
  </li>
  </ol>

  </div>
  <!-- do not display if the question index exceeds the length of all quizez -->
  <div v-if="questionindex < quizez.length">
  <!-- display only if the question index is greater than zero -->
  <!-- onclick of this button, call the previous function, and show last question -->
  <button v-if="questionindex > 0" v-on:click="prev">
  prev
  </button>
 <!-- onclick of this button, call the next function, and show next question -->
  <button v-on:click="next">
  next
</button>
</div>
<!-- show total score, if the questions are completed -->
<span v-if="questionindex == quizez.length">Your Total score is {{score}} / {{quizez.length}}</span>

</div>
</template>

If we take a look at the template above, what we are doing is very simple.

We use the v-for loop to loop through all our questions, we then put a v-show condition, to show only the current question. Within the loop, we then display the category, after which we display the question, then we proceed to loop through all the options in that question using another v-for loop, binding all current questions to the index of the answers we have already created for them, and also binding their values, to the option itself.

After this, we put up a condition that does not display the next and previous buttons if all the questions have been completed.

We also put a condition on the previous button, to only show if the current question isn't the first question in the array. After this, we bind the click event, to the Prev method we had defined earlier.

Similarly, we will add a click event to the next button and bind it to the next function we had defined earlier too.

Finally, we will create a span that shows the total amount of points scored once there are no more questions to create.

At this point, our quiz app is completed and looks this way:

Packaging Electron To Use The Production Ready App

Right now, the application still runs from the development server. It is now time to run the app from the file directly, to be packaged with Electron.

On the terminal running the Vue server, hit ctrl+c, then run the following command:

npm run build

After running this, a file would be created in your root folder, under dist/index.html. Open up the file, remove all leading / from all references to JS or CSS files, then close. If we don't do this, our application would load only an empty screen, as it would not be able to locate our CSS and JavaScript files.

At this point, let's go back into our elect.js, and add some configurations to make it serve from the file. At the top of the file, add these two imports:

var url = require('url')
const path = require('path');

Here, we are just defining some imports. Lets see how we'll use them later:

Lets replace the part that says win.loadURL('http://localhost:8080') with the code below:

win.loadURL(url.format({
  pathname: path.join(__dirname, 'dist/index.html'),
  protocol: 'file:',
  slashes: true
  }));

What we have done here is to tell Electron to load the index.html in our dist folder, and it should attempt to load it as a file rather than an actual url.

Once done, save and hit electron .

At this point, we have our app running as seen below: To distribute to various platforms, you can follow the official guide on Electron's Github page on how to package for various distributions here

Conclusion

At this point, we have seen how to build a desktop application using Electron and Vue.

In the course of our tutorial, we learned that Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. We also learned that we chose to use Electron because it is cross platform and is easier to use than the likes of C# or Java.

At this point, we should be able to build amazing cross platform applications using Electron and Vue in less time while aiming for perfection.

Ogundipe Samuel Ayo

7 posts

Self Taught Software Developer. Software Developer At Crust Resources Conversant with Php (Codeigniter and Laravel), Python (Flask and Django), C# (WPF), Javacript (Vue, Angular, React-native, Node.js (Adonis.js, Express) ). Can Use the Mean Stack, But never used it For anything Serious