Make a Windows App with Vue.js and Electron
Tags: vue, javascript, tutorial
Published: 2019-12-28 21:15:56

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

**Follow me on Twitter at https://twitter.com/AuMayeung**

**Many more articles at https://medium.com/@hohanga**

For web developers, making a Windows app requires a significant learning curve. However, now there is a solution to convert a JavaScript web app to Windows app with Electron without too much work. React can easily combined with Electron to build a Windows app.

Electron is a Chromium browser-based runtime that allows us to run web apps like a native desktop app. It wraps Chromium around our web app so that it looks like it’s a desktop app. We can make some calls to native APIs like file manipulating, modifying the menus, and interact with some hardware.

Building an Electron Vue.js app is easy if we use the [vue-cli-plugin-electron-builder](https://github.com/nklayman/vue-cli-plugin-electron-builder) add-on for Vue CLI 3. It is from [https://github.com/nklayman/vue-cli-plugin-electron-builder](https://github.com/nklayman/vue-cli-plugin-electron-builder).

In this article, we will build a Vue Electron app that runs on Windows. It is an address book app that allows us to add contacts and save them with a back end serving a JSON file.

To start building the app, we start by installing Vue CLI by running:

```bash
npm i -g @vue/cli
```

Next, we create our Vue.js project by running `vue create address-book-app` . Be sure to select ‘Manually select features’, and after that choose to include Babel, Vuex, and Vue Router. This will create the initial files for our app. Next, we add Electron to our app by running:

```bash
vue add electron-builder
```

This command adds the necessary files and scripts for us to build our app into an Electron app, and also preview our Vue.js app running in the Electron window, allowing us to debug within there instead of a regular browser.

Once we add that, we need to add our own libraries. We need Axios for making HTTP requests, Bootstrap-Vue for styling, and Vee-Validate for form validation. We install these by running:

```bash
npm i axios bootstrap-vue vee-validate
```

in the project folder.

Now that we installed our libraries, we can start building our address book app. We start by creating the contact form for adding and editing our contacts. We add a `ContactFome.vue` file into the `components` folder and add:

```javascript
<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form [@submit](http://twitter.com/submit).prevent="onSubmit" novalidate>
      <b-form-group label="First Name">
        <ValidationProvider name="firstName" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.firstName"
            required
            placeholder="First Name"
            name="firstName"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">First name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group> <b-form-group label="Last Name">
        <ValidationProvider name="lastName" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.lastName"
            required
            placeholder="Last Name"
            name="lastName"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Last name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group> <b-form-group label="Address">
        <ValidationProvider name="addressLineOne" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.addressLineOne"
            required
            placeholder="Address"
            name="addressLineOne"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Address is required.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group> <b-form-group label="City">
        <ValidationProvider name="city" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.city"
            required
            placeholder="City"
            name="city"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">City is required.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group> <b-form-group label="Postal Code">
        <ValidationProvider
          name="postalCode"
          rules="required|postal_code:country"
          v-slot="{ errors }"
        >
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.postalCode"
            required
            placeholder="Postal Code"
            name="postalCode"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Postal code is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group> <b-form-group label="Country">
        <ValidationProvider name="country" rules="required" v-slot="{ errors }">
          <b-form-select
            :options="countries"
            :state="errors.length == 0"
            v-model="form.country"
            required
            placeholder="Country"
            name="country"
          ></b-form-select>
          <b-form-invalid-feedback :state="errors.length == 0">Country is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group> <b-form-group label="Email">
        <ValidationProvider name="email" rules="required|email" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.email"
            required
            placeholder="Email"
            name="email"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group> <b-form-group label="Phone">
        <ValidationProvider name="phone" rules="required|phone:country" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.phone"
            required
            placeholder="Phone"
            name="phone"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group> <b-form-group label="Age">
        <ValidationProvider
          name="age"
          rules="required|min_value:0|max_value:200"
          v-slot="{ errors }"
        >
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.age"
            required
            placeholder="Age"
            name="age"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group><b-button type="submit" variant="primary">Submit</b-button>
      <b-button type="reset" variant="danger" [@click](http://twitter.com/click)="cancel()">Cancel</b-button>
    </b-form>
  </ValidationObserver>
</template>

<script>
import { COUNTRIES } from "@/helpers/exports";
import { requestsMixin } from "@/mixins/requestsMixin";export default {
  name: "ContactForm",
  mixins: [requestsMixin],
  props: {
    edit: Boolean,
    contact: Object
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }if (this.edit) {
        await this.editContact(this.form);
      } else {
        await this.addContact(this.form);
      }
      const response = await this.getContacts();
      this.$store.commit("setContacts", response.data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  data() {
    return {
      form: {},
      countries: COUNTRIES.map(c => ({ value: c.name, text: c.name }))
    };
  },
  watch: {
    contact: {
      handler(c) {
        this.form = c || {};
      },
      deep: true,
      immediate: true
    }
  }
};
</script>
```

In the form, we wrap each input with `ValidationProvider` so that we get form validation for each field, along with the form validation errors. We add `:state=”errors.length == 0"` in each `b-form-input` so that we get the right validation message displayed and styled properly for each input. The `errors` object has the form of validation error messages for each input. We also need to specify the `name` prop in `ValidationProvider` and `b-form-input` so that form validation rules are applied to the input inside the `ValidationProvider` .

We use `ValidationObserver` to watch for validation errors in our form which is wrapped inside. We have the `ref=”observer”` prop in the `ValidationObserver` so that we can call `await this.$refs.observer.validate();` to validate our form. `observer` is our ref for the `ValidationObserver` component. We put the form inside the `ValidationObserver` component here to let us validate the whole form. With Vee-Validate, we get the `this.$refs.observer.validate()` function when we use `ValidationObserver` like we did in the code above. It returns a promise that resolves to true if the form is valid and false otherwise. So if it resolves to false, we don’t run the rest of the function’s code.

In this form, there is cross-field validation. The country field is checked before checking the phone number and postal code formats. We will add those validation rules into `main.js` later.

To display the form validation error messages, we have the `errors` object available in the template only. The scoped slots built into the Vee-Validate components provides the `errors` object, which has the validation messages.

In the `rules` prop of each field, we passing the rule names separated by pipes. The `phone` and `postal_code` rules are cross field rules. The `country` after the colon is the name prop of the `country` field, which is `country` .

The form and inputs components are all provided by BootstrapVue.

When the form submit button is clicked, we call the `onSubmit` button. The `onSubmit` function is passed into the `submit.prevent` prop to prevent the default submit action so we can use Ajax to submit the form.

In this function, we use the `this.$refs.observer.validate();` to validate the form. Then after that, we call `editContact` or `addContact` depending if `edit` prop is true or not. We passed those in from the `HomePage.vue` file which we will add. These 2 functions are for making HTTP requests to our server to submit our data.

Once either of the function is called, we get the latest data and put them into our Vuex store with:

```javascript
this.$store.commit("setContacts", response.data);
```

`this.$store` is provided by Vuex.

Then we emit the `saved` event to `HomePage.vue` to close the modals.

The countries are imported from another file and the `contact` prop is passed in from `HomePage.vue` when the user selects an entry to edit.

Next create a `helpers` folder in the `src` folder and add an `exports.js` file. In there, add:

```javascript
export const COUNTRIES = [
  { name: "Afghanistan", code: "AF" },
  { name: "Aland Islands", code: "AX" },
  { name: "Albania", code: "AL" },
  { name: "Algeria", code: "DZ" },
  { name: "American Samoa", code: "AS" },
  { name: "AndorrA", code: "AD" },
  { name: "Angola", code: "AO" },
  { name: "Anguilla", code: "AI" },
  { name: "Antarctica", code: "AQ" },
  { name: "Antigua and Barbuda", code: "AG" },
  { name: "Argentina", code: "AR" },
  { name: "Armenia", code: "AM" },
  { name: "Aruba", code: "AW" },
  { name: "Australia", code: "AU" },
  { name: "Austria", code: "AT" },
  { name: "Azerbaijan", code: "AZ" },
  { name: "Bahamas", code: "BS" },
  { name: "Bahrain", code: "BH" },
  { name: "Bangladesh", code: "BD" },
  { name: "Barbados", code: "BB" },
  { name: "Belarus", code: "BY" },
  { name: "Belgium", code: "BE" },
  { name: "Belize", code: "BZ" },
  { name: "Benin", code: "BJ" },
  { name: "Bermuda", code: "BM" },
  { name: "Bhutan", code: "BT" },
  { name: "Bolivia", code: "BO" },
  { name: "Bosnia and Herzegovina", code: "BA" },
  { name: "Botswana", code: "BW" },
  { name: "Bouvet Island", code: "BV" },
  { name: "Brazil", code: "BR" },
  { name: "British Indian Ocean Territory", code: "IO" },
  { name: "Brunei Darussalam", code: "BN" },
  { name: "Bulgaria", code: "BG" },
  { name: "Burkina Faso", code: "BF" },
  { name: "Burundi", code: "BI" },
  { name: "Cambodia", code: "KH" },
  { name: "Cameroon", code: "CM" },
  { name: "Canada", code: "CA" },
  { name: "Cape Verde", code: "CV" },
  { name: "Cayman Islands", code: "KY" },
  { name: "Central African Republic", code: "CF" },
  { name: "Chad", code: "TD" },
  { name: "Chile", code: "CL" },
  { name: "China", code: "CN" },
  { name: "Christmas Island", code: "CX" },
  { name: "Cocos (Keeling) Islands", code: "CC" },
  { name: "Colombia", code: "CO" },
  { name: "Comoros", code: "KM" },
  { name: "Congo", code: "CG" },
  { name: "Congo, The Democratic Republic of the", code: "CD" },
  { name: "Cook Islands", code: "CK" },
  { name: "Costa Rica", code: "CR" },
  {
    name: 'Cote D"Ivoire',
    code: "CI"
  },
  { name: "Croatia", code: "HR" },
  { name: "Cuba", code: "CU" },
  { name: "Cyprus", code: "CY" },
  { name: "Czech Republic", code: "CZ" },
  { name: "Denmark", code: "DK" },
  { name: "Djibouti", code: "DJ" },
  { name: "Dominica", code: "DM" },
  { name: "Dominican Republic", code: "DO" },
  { name: "Ecuador", code: "EC" },
  { name: "Egypt", code: "EG" },
  { name: "El Salvador", code: "SV" },
  { name: "Equatorial Guinea", code: "GQ" },
  { name: "Eritrea", code: "ER" },
  { name: "Estonia", code: "EE" },
  { name: "Ethiopia", code: "ET" },
  { name: "Falkland Islands (Malvinas)", code: "FK" },
  { name: "Faroe Islands", code: "FO" },
  { name: "Fiji", code: "FJ" },
  { name: "Finland", code: "FI" },
  { name: "France", code: "FR" },
  { name: "French Guiana", code: "GF" },
  { name: "French Polynesia", code: "PF" },
  { name: "French Southern Territories", code: "TF" },
  { name: "Gabon", code: "GA" },
  { name: "Gambia", code: "GM" },
  { name: "Georgia", code: "GE" },
  { name: "Germany", code: "DE" },
  { name: "Ghana", code: "GH" },
  { name: "Gibraltar", code: "GI" },
  { name: "Greece", code: "GR" },
  { name: "Greenland", code: "GL" },
  { name: "Grenada", code: "GD" },
  { name: "Guadeloupe", code: "GP" },
  { name: "Guam", code: "GU" },
  { name: "Guatemala", code: "GT" },
  { name: "Guernsey", code: "GG" },
  { name: "Guinea", code: "GN" },
  { name: "Guinea-Bissau", code: "GW" },
  { name: "Guyana", code: "GY" },
  { name: "Haiti", code: "HT" },
  { name: "Heard Island and Mcdonald Islands", code: "HM" },
  { name: "Holy See (Vatican City State)", code: "VA" },
  { name: "Honduras", code: "HN" },
  { name: "Hong Kong", code: "HK" },
  { name: "Hungary", code: "HU" },
  { name: "Iceland", code: "IS" },
  { name: "India", code: "IN" },
  { name: "Indonesia", code: "ID" },
  { name: "Iran, Islamic Republic Of", code: "IR" },
  { name: "Iraq", code: "IQ" },
  { name: "Ireland", code: "IE" },
  { name: "Isle of Man", code: "IM" },
  { name: "Israel", code: "IL" },
  { name: "Italy", code: "IT" },
  { name: "Jamaica", code: "JM" },
  { name: "Japan", code: "JP" },
  { name: "Jersey", code: "JE" },
  { name: "Jordan", code: "JO" },
  { name: "Kazakhstan", code: "KZ" },
  { name: "Kenya", code: "KE" },
  { name: "Kiribati", code: "KI" },
  {
    name: 'Korea, Democratic People"S Republic of',
    code: "KP"
  },
  { name: "Korea, Republic of", code: "KR" },
  { name: "Kuwait", code: "KW" },
  { name: "Kyrgyzstan", code: "KG" },
  {
    name: 'Lao People"S Democratic Republic',
    code: "LA"
  },
  { name: "Latvia", code: "LV" },
  { name: "Lebanon", code: "LB" },
  { name: "Lesotho", code: "LS" },
  { name: "Liberia", code: "LR" },
  { name: "Libyan Arab Jamahiriya", code: "LY" },
  { name: "Liechtenstein", code: "LI" },
  { name: "Lithuania", code: "LT" },
  { name: "Luxembourg", code: "LU" },
  { name: "Macao", code: "MO" },
  { name: "Macedonia, The Former Yugoslav Republic of", code: "MK" },
  { name: "Madagascar", code: "MG" },
  { name: "Malawi", code: "MW" },
  { name: "Malaysia", code: "MY" },
  { name: "Maldives", code: "MV" },
  { name: "Mali", code: "ML" },
  { name: "Malta", code: "MT" },
  { name: "Marshall Islands", code: "MH" },
  { name: "Martinique", code: "MQ" },
  { name: "Mauritania", code: "MR" },
  { name: "Mauritius", code: "MU" },
  { name: "Mayotte", code: "YT" },
  { name: "Mexico", code: "MX" },
  { name: "Micronesia, Federated States of", code: "FM" },
  { name: "Moldova, Republic of", code: "MD" },
  { name: "Monaco", code: "MC" },
  { name: "Mongolia", code: "MN" },
  { name: "Montenegro", code: "ME" },
  { name: "Montserrat", code: "MS" },
  { name: "Morocco", code: "MA" },
  { name: "Mozambique", code: "MZ" },
  { name: "Myanmar", code: "MM" },
  { name: "Namibia", code: "NA" },
  { name: "Nauru", code: "NR" },
  { name: "Nepal", code: "NP" },
  { name: "Netherlands", code: "NL" },
  { name: "Netherlands Antilles", code: "AN" },
  { name: "New Caledonia", code: "NC" },
  { name: "New Zealand", code: "NZ" },
  { name: "Nicaragua", code: "NI" },
  { name: "Niger", code: "NE" },
  { name: "Nigeria", code: "NG" },
  { name: "Niue", code: "NU" },
  { name: "Norfolk Island", code: "NF" },
  { name: "Northern Mariana Islands", code: "MP" },
  { name: "Norway", code: "NO" },
  { name: "Oman", code: "OM" },
  { name: "Pakistan", code: "PK" },
  { name: "Palau", code: "PW" },
  { name: "Palestinian Territory, Occupied", code: "PS" },
  { name: "Panama", code: "PA" },
  { name: "Papua New Guinea", code: "PG" },
  { name: "Paraguay", code: "PY" },
  { name: "Peru", code: "PE" },
  { name: "Philippines", code: "PH" },
  { name: "Pitcairn", code: "PN" },
  { name: "Poland", code: "PL" },
  { name: "Portugal", code: "PT" },
  { name: "Puerto Rico", code: "PR" },
  { name: "Qatar", code: "QA" },
  { name: "Reunion", code: "RE" },
  { name: "Romania", code: "RO" },
  { name: "Russian Federation", code: "RU" },
  { name: "RWANDA", code: "RW" },
  { name: "Saint Helena", code: "SH" },
  { name: "Saint Kitts and Nevis", code: "KN" },
  { name: "Saint Lucia", code: "LC" },
  { name: "Saint Pierre and Miquelon", code: "PM" },
  { name: "Saint Vincent and the Grenadines", code: "VC" },
  { name: "Samoa", code: "WS" },
  { name: "San Marino", code: "SM" },
  { name: "Sao Tome and Principe", code: "ST" },
  { name: "Saudi Arabia", code: "SA" },
  { name: "Senegal", code: "SN" },
  { name: "Serbia", code: "RS" },
  { name: "Seychelles", code: "SC" },
  { name: "Sierra Leone", code: "SL" },
  { name: "Singapore", code: "SG" },
  { name: "Slovakia", code: "SK" },
  { name: "Slovenia", code: "SI" },
  { name: "Solomon Islands", code: "SB" },
  { name: "Somalia", code: "SO" },
  { name: "South Africa", code: "ZA" },
  { name: "South Georgia and the South Sandwich Islands", code: "GS" },
  { name: "Spain", code: "ES" },
  { name: "Sri Lanka", code: "LK" },
  { name: "Sudan", code: "SD" },
  { name: "Suriname", code: "SR" },
  { name: "Svalbard and Jan Mayen", code: "SJ" },
  { name: "Swaziland", code: "SZ" },
  { name: "Sweden", code: "SE" },
  { name: "Switzerland", code: "CH" },
  { name: "Syrian Arab Republic", code: "SY" },
  { name: "Taiwan, Province of China", code: "TW" },
  { name: "Tajikistan", code: "TJ" },
  { name: "Tanzania, United Republic of", code: "TZ" },
  { name: "Thailand", code: "TH" },
  { name: "Timor-Leste", code: "TL" },
  { name: "Togo", code: "TG" },
  { name: "Tokelau", code: "TK" },
  { name: "Tonga", code: "TO" },
  { name: "Trinidad and Tobago", code: "TT" },
  { name: "Tunisia", code: "TN" },
  { name: "Turkey", code: "TR" },
  { name: "Turkmenistan", code: "TM" },
  { name: "Turks and Caicos Islands", code: "TC" },
  { name: "Tuvalu", code: "TV" },
  { name: "Uganda", code: "UG" },
  { name: "Ukraine", code: "UA" },
  { name: "United Arab Emirates", code: "AE" },
  { name: "United Kingdom", code: "GB" },
  { name: "United States", code: "US" },
  { name: "United States Minor Outlying Islands", code: "UM" },
  { name: "Uruguay", code: "UY" },
  { name: "Uzbekistan", code: "UZ" },
  { name: "Vanuatu", code: "VU" },
  { name: "Venezuela", code: "VE" },
  { name: "Viet Nam", code: "VN" },
  { name: "Virgin Islands, British", code: "VG" },
  { name: "Virgin Islands, U.S.", code: "VI" },
  { name: "Wallis and Futuna", code: "WF" },
  { name: "Western Sahara", code: "EH" },
  { name: "Yemen", code: "YE" },
  { name: "Zambia", code: "ZM" },
  { name: "Zimbabwe", code: "ZW" }
];
```

so we can have a list of countries in the Countries field drop down in `ContactForm.vue`.

Next we add the mixin that we referenced in `ContactForm.vue` . Create a `mixins` folder in the `src` folder and add a `requestsMixin.js` file. In there add:

```javascript
const APIURL = "[http://localhost:3000](http://localhost:3000)";
const axios = require("axios");export const requestsMixin = {
  methods: {
    getContacts() {
      return axios.get(`${APIURL}/contacts`);
    }, 

    addContact(data) {
      return axios.post(`${APIURL}/contacts`, data);
    }, 

    editContact(data) {
      return axios.put(`${APIURL}/contacts/${data.id}`, data);
    }, 

    deleteContact(id) {
      return axios.delete(`${APIURL}/contacts/${id}`);
    }
  }
};
```

These are functions for returning promises for the requests that we make to our back end.

Next in `Home.vue` , replace the existing code with the following:

```javascript
<template>
  <div class="page">
    <h1 class="text-center">Address Book</h1>
    <b-button-toolbar>
      <b-button [@click](http://twitter.com/click)="openAddModal()">Add Contact</b-button>
      <b-button [@click](http://twitter.com/click)="getAllContacts()">Refresh</b-button>
    </b-button-toolbar>
    <br />
    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th>First Name</b-th>
          <b-th>Last Name</b-th>
          <b-th>Address</b-th>
          <b-th>Phone</b-th>
          <b-th>Email</b-th>
          <b-th>Age</b-th>
          <b-th></b-th>
          <b-th></b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="c in contacts" :key="c.id">
          <b-td>{{c.firstName}}</b-td>
          <b-td>{{c.lastName}}</b-td>
          <b-td>{{c.addressLineOne}}, {{c.city}}, {{c.region}}, {{c.country}}, {{c.postalCode}}</b-td>
          <b-td>{{c.phone}}</b-td>
          <b-td>{{c.email}}</b-td>
          <b-td>{{c.age}}</b-td>
          <b-td>
            <b-button [@click](http://twitter.com/click)="openEditModal(c)">Edit</b-button>
          </b-td>
          <b-td>
            <b-button [@click](http://twitter.com/click)="deleteOneContact(c.id)">Delete</b-button>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple><b-modal id="add-modal" title="Add Contact" hide-footer>
      <ContactForm [@saved](http://twitter.com/saved)="closeModal()" [@cancelled](http://twitter.com/cancelled)="closeModal()" :edit="false"></ContactForm>
    </b-modal><b-modal id="edit-modal" title="Edit Contact" hide-footer>
      <ContactForm
        [@saved](http://twitter.com/saved)="closeModal()"
        [@cancelled](http://twitter.com/cancelled)="closeModal()"
        :edit="true"
        :contact="selectedContact"
      ></ContactForm>
    </b-modal>
  </div>
</template>

<script>
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { requestsMixin } from "@/mixins/requestsMixin";
import ContactForm from "@/components/ContactForm";export default {
  name: "home",
  mixins: [requestsMixin],
  components: {
    ContactForm
  },
  computed: {
    contacts() {
      return this.$store.state.contacts;
    }
  },
  beforeMount() {
    this.getAllContacts();
  },
  data() {
    return {
      selectedContact: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(contact) {
      this.$bvModal.show("edit-modal");
      this.selectedContact = contact;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedContact = {};
    },
    async deleteOneContact(id) {
      await this.deleteContact(id);
      this.getAllContacts();
    },
    async getAllContacts() {
      const response = await this.getContacts();
      this.$store.commit("setContacts", response.data);
    }
  }
};
</script>

<style scoped>
#add-button {
  margin-bottom: 20px;
}
</style>
```

We have a table for displaying the list of contacts from the store. This component watches for our Vuex store updates by getting them from the `contacts` property in the `computed` field. The latest Vuex store data are always returned there.

The data is loaded when the page first loads with the `getAllContacts` function call in the `beforeMount` hook. `getAllContacts` set the contacts in the store after it’s retrieved from the back end.

We have buttons to open and close the modals which contain our contact form. Note that we have to set the form to pass into the `contact` prop in the `ContactForm` in the edit modal by setting `this.selectedContact` in the `openEditModal` with the passed in `contact` argument. `openEditModal` is used by the Edit button in each row of the table.

In each row of the table, there’s also a Delete button, we pass in the ID of the contact in there so that we can delete it by ID.

Next in `App.vue`, we replace the existing code with:

```javascript
<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">Address Book</b-navbar-brand><b-navbar-toggle target="nav-collapse"></b-navbar-toggle><b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template><script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>

<style lang="scss">
.page {
  padding: 20px;
}button {
  margin-right: 10px;
}
</style>
```

In this file, we add the BootstrapVue `navbar` component and highlight the links by checking the `path` which we get in the `watch` block. The `active` prop is where the highlighting is set. If `active` is `true` then the link will be highlighted. We choose to highlight the link if the `path` is equal to the route the user is in.

In the `style` block, we add some padding to our page and margins to our buttons.

In `main.js` replace the existing code with:

```javascript
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required, email, min_value, max_value } from "vee-validate/dist/rules";
extend("required", required);
extend("email", email);
extend("min_value", min_value);
extend("max_value", max_value);
extend("phone", {
  validate: (value, { country }) => {
    if (["United States", "Canada"].includes(country)) {
      return /^((d{3})|d{3})-?d{3}-?d{4}$/.test(value);
    }
    return true;
  },
  message: "Phone number is invalid.",
  params: [{ name: "country", isTarget: true }]
});
extend("postal_code", {
  validate: (value, { country }) => {
    if ("United States" == country) {
      return /^[0-9]{5}(?:-[0-9]{4})?$/.test(value);
    } else if ("Canada" == country) {
      return /^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/.test(value);
    }
    return true;
  },
  message: "Phone number is invalid.",
  params: [{ name: "country", isTarget: true }]
});

Vue.config.productionTip = false;
Vue.use(BootstrapVue);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);

new Vue({
  router,
  store,
  render: h => h(App),
  mounted() {
    this.$router.push("/");
  }
})
.$mount("#app");
```

We have the Vee-Validate validation rules added here and register our BootstrapVue components, and Vee-Validate validation components here so we can use it in our templates.

Note that we have:

```javascript
mounted() {
  this.$router.push("/");
}
```

in the object passed into the `Vue` constructor so that our built Windows app won’t show a blank page.

In `router.js` we replace the existing code with:

```javascript
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";Vue.use(Router);export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});
```

to let us go to `Home.vue` .

In `store.js` , replace the existing code with:

```javascript
import Vue from "vue";
import Vuex from "vuex";Vue.use(Vuex);export default new Vuex.Store({
  state: {
    contacts: []
  },
  mutations: {
    setContacts(state, payload) {
      state.contacts = payload;
    }
  },
  actions: {}
});
```

so that we can store contacts in the store for easy access by all components.

Now we can run `npm run electron:serve` to run the app.

To start the back end, we first install the `json-server` package by running `npm i json-server`. Then, go to our project folder and run:

```bash
json-server --watch db.json
```

In `db.json`, change the text to:

```javascript
{
  "contacts": [
  ]
}
```

So we have the `contacts` endpoints defined in the `requests.js` available.