Categories
JavaScript Vue

Form Validation with Vee-Validate

Spread the love

Vue.js is a great framework for building front end web apps. It uses a component-based architecture which makes organizing code easy. It allows you to use the latest features JavaScript has to offer which means writing code to build your apps is easy than ever. It has a lot of add-ons like routing and flux store that you can add when you scaffold your app.

However, one thing that is missing is form validation. This means that we have to find our own form validation library to do form validation or write the form validation code ourselves.

If we choose to use a library to do form validation, Vee-Validate is a great choice plugging directly into Vue.js form code to do form validation. Vee-Validate primary adds code to Vue.js component templates to enable form validation for Vue.js forms. It has form validation rules for many kinds of inputs. Therefore, it is a great choice for validating Vue.js forms.

In this story, we will build an address book app with Vue.js that uses Vee-Validate to validate our inputs. The form allows us to add and edit our contacts; also, we can get and delete contacts.

To build our app, first, we need to quickly set up a back end. To do this, we use a Node.js package called JSON Server to run our back end. Find the package’s documentation here.

Once this is running, it provides us with routes for us to save our contact entries from front end.

To install the package, run:

npm install -g json-server

We will run this later so we can save our contacts.

Now we can start building our app. To do this, install the Vue CLI by running:

npm install -g @vue/cli

Then create the app by running:

vue create vee-validate-address-book-app

vee-validate-address-book-app is our app name. When running the wizard, be sure you choose to include Vuex and Vue Router as we will use it later. Next, we have to install some libraries. We need an HTTP client, a Material Design library for making our app look good, and the Vee-Validate library.

To do this, run npm i axios vee-validate vue-material . Axios is our HTTP client for communicating to back end. Vue Material is our Material Design library.

Next, we create our components that we nest in our page components. To do this, create a components folder in our project folder and create a file within it called ContactForm.vue .

In this file, we put:

<template>
  <div class="contact-form">
    <div class="center">
      <h1>{{editing ? 'Edit': 'Add'}} Contact</h1>
    </div>
    <form novalidate class="md-layout" @submit="save">
      <md-field :class="{ 'md-invalid': errors.has('firstName') }">
        <label for="firstName">First Name</label>
        <md-input
          name="firstName"
          v-model="contact.firstName"
          v-validate="'required'"
          :disabled="sending"
        />
        <span class="md-error" v-if="errors.has('firstName')">First Name is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('lastName') }">
        <label for="lastName">Last Name</label>
        <md-input
          name="lastName"
          v-model="contact.lastName"
          :disabled="sending"
          v-validate="'required'"
        />
        <span class="md-error" v-if="errors.has('lastName')">Last Name is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('addressLineOne') }">
        <label for="addressLineOne">Address Line 1</label>
        <md-input
          name="addressLineOne"
          v-model="contact.addressLineOne"
          :disabled="sending"
          v-validate="'required'"
        />
        <span class="md-error" v-if="errors.has('addressLineOne')">Address line 1 is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('addressLineTwo') }">
        <label for="addressLineTwo">Address Line 2</label>
        <md-input name="addressLineTwo" v-model="contact.addressLineTwo" :disabled="sending" />
        <span class="md-error" v-if="errors.has('addressLineTwo')">Address line 2 is required</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('city') }">
        <label for="city">City</label>
        <md-input name="city" v-model="contact.city" :disabled="sending" v-validate="'required'" />
        <span class="md-error" v-if="errors.has('city')">City is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('country') }">
        <label for="country">Country</label>
        <md-select
          name="country"
          v-model="contact.country"
          md-dense
          :disabled="sending"
          v-validate.continues="'required'"
        >
          <md-option :value="c" :key="c" v-for="c in countries">{{c}}</md-option>
        </md-select>
        <span class="md-error" v-if="errors.firstByRule('country', 'required')">Country is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('postalCode') }">
        <label for="postalCode">Postal Code</label>
        <md-input
          name="postalCode"
          v-model="contact.postalCode"
          :disabled="sending"
          v-validate="{ required: true, regex: getPostalCodeRegex() }"
        />
        <span
          class="md-error"
          v-if="errors.firstByRule('postalCode', 'required')"
        >Postal Code is required.</span>
        <span
          class="md-error"
          v-if="errors.firstByRule('postalCode', 'regex')"
        >Postal Code is invalid.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('phone') }">
        <label for="phone">Phone</label>
        <md-input
          name="phone"
          v-model="contact.phone"
          :disabled="sending"
          v-validate="{ required: true, regex: getPhoneRegex() }"
        />
        <span class="md-error" v-if="errors.firstByRule('phone', 'required')">Phone is required.</span>
        <span class="md-error" v-if="errors.firstByRule('phone', 'regex')">Phone is invalid.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('gender') }">
        <label for="gender">Gender</label>
        <md-select
          name="gender"
          v-model="contact.gender"
          md-dense
          :disabled="sending"
          v-validate.continues="'required'"
        >
          <md-option value="male">Male</md-option>
          <md-option value="female">Female</md-option>
        </md-select>
        <span class="md-error" v-if="errors.firstByRule('gender', 'required')">Gender is required.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('age') }">
        <label for="age">Age</label>
        <md-input
          type="number"
          id="age"
          name="age"
          autocomplete="age"
          v-model="contact.age"
          :disabled="sending"
          v-validate="'required|between:0,200'"
        />
        <span class="md-error" v-if="errors.firstByRule('age', 'required')">Age is required.</span>
        <span class="md-error" v-if="errors.firstByRule('age', 'between')">Age must be 0 and 200.</span>
      </md-field>
      <br />
      <md-field :class="{ 'md-invalid': errors.has('email') }">
        <label for="email">Email</label>
        <md-input
          type="email"
          name="email"
          autocomplete="email"
          v-model="contact.email"
          :disabled="sending"
          v-validate="'required|email'"
        />
        <span class="md-error" v-if="errors.firstByRule('email', 'required')">Email is required.</span>
        <span class="md-error" v-if="errors.firstByRule('email', 'email')">Email is invalid.</span>
      </md-field>
<md-progress-bar md-mode="indeterminate" v-if="sending" />
<md-button type="submit" class="md-raised">{{editing ? 'Edit':'Create'}} Contact</md-button>
    </form>
  </div>
</template>

<script>
import { COUNTRIES } from "@/helpers/exports";
import { contactMixin } from "@/mixins/contactMixin";
export default {
  name: "ContactForm",
  mixins: [contactMixin],
  props: {
    editing: Boolean,
    contactId: Number
  },
  computed: {
    isFormDirty() {
      return Object.keys(this.fields).some(key => this.fields[key].dirty);
    },
    contacts() {
      return this.$store.state.contacts;
    }
  },
  data() {
    return {
      sending: false,
      contact: {},
      countries: COUNTRIES.map(c => c.name)
    };
  },
  beforeMount() {
    this.contact = this.contacts.find(c => c.id == this.contactId) || {};
  },
  methods: {
    async save(evt) {
      evt.preventDefault();
      try {
        const result = await this.$validator.validateAll();
        if (!result) {
          return;
        }
        if (this.editing) {
          await this.updateContact(this.contact, this.contactId);
          await this.getAllContacts();
          this.$emit("contactSaved");
        } else {
          await this.addContact(this.contact);
          await this.getAllContacts();
          this.$router.push("/");
        }
      } catch (ex) {
        console.log(ex);
      }
    },
    async getAllContacts() {
      try {
        const response = await this.getContacts();
        this.$store.commit("setContacts", response.data);
      } catch (ex) {
        console.log(ex);
      }
    },
    getPostalCodeRegex() {
      if (this.contact.country == "United States") {
        return /^[0-9]{5}(?:-[0-9]{4})?$/;
      } else if (this.contact.country == "Canada") {
        return /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/;
      }
      return /./;
    },
    getPhoneRegex() {
      if (["United States", "Canada"].includes(this.contact.country)) {
        return /^[2-9]\d{2}[2-9]\d{2}\d{4}$/;
      }
      return /./;
    }
  }
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.contact-form {
  margin: 0 auto;
  width: 90%;
}
</style>

In the file above, we have the contact form for adding and updating contacts in our address book — where Vee-Validate is used the most. Notice that in most input controls within the form tag, we have the v-validate prop.

This is where we specify what kind of input the control accepts.

required means that the form field is required.

regex means we validate against a specified regular expression.

This allows for custom form validation where there are no built-in rules for Vee-Validate available, or when you need to validate the field differently, depending on the value of another field.

For example for a phone number, we have this function:

getPhoneRegex() {  
  if (["United States", "Canada"].includes(this.contact.country)){  
    return /^\[2-9\]\\d{2}\[2-9\]\\d{2}\\d{4}$/;  
  }  
  return /./;  
}

It allows us to validate the number to see if it matches the North American telephone format when we enter United States or Canada. Otherwise, we let people enter whatever they want.

Similarly, for postal code, we have:

getPostalCodeRegex() {  
  if (this.contact.country == "United States") {  
    return /^[0-9]{5}(?:-\[0-9\]{4})?$/;  
  } 
  else if (this.contact.country == "Canada") {  
    return /^[A-Za-z]\d\[A-Za-z]\[ -]?\d\[A-Za-z]\d$/;  
  }  
  return /./;  
}

This allows us to check for U.S. and Canadian postal codes.

To display errors, we can check if errors exist for a form field, then display them. For example, for a first name, we have:

<span class="md-error" v-if="errors.has('firstName')">First Name is required.</span>

errors.has(‘firstName’) checks if the first name field meets the specified validation criteria. Since we’re checking if it’s filled in, there is only one possible error, so we can just display the only error when errors.has(‘firstName’) returns true .

For something more complex like a phone number, we have:

<span class="md-error" v-if="errors.firstByRule('phone', 'required')">Phone is required.</span>
<span class="md-error" v-if="errors.firstByRule('phone', 'regex')">Phone is invalid.</span>

This allows us to check for each validation rule separately. For the phone number field, we have to check if it’s filled in and if what’s filled in has a valid format. The errors.firstByRule function allows us to do that.

errors.firstByRule(‘phone’, ‘required’) returns true if the field is not filled in and false otherwise.

errors.firstByRule(‘phone’, ‘regex’) returns true is the phone number format is filled in incorrectly and false otherwise.

Vee-Validate provides a this.field object to your component. So we can check if fields are dirty, meaning whether they have been manipulated or not, by adding:

Object.keys(this.fields).some(key => this.fields\[key\].dirty)

Each property is a form field and each property of the this.fields object has a dirty property, so we can check whether fields are manipulated or not.

In the save function of the methods object, we have:

async save(evt) {  
  evt.preventDefault();  
  try {  
    const result = await this.$validator.validateAll();  
    if (!result) {  
      return;  
    }  
    if (this.editing) {  
      await this.updateContact(this.contact, this.contactId);  
      await this.getAllContacts();  
      this.$emit("contactSaved");  
    } 
    else {  
      await this.addContact(this.contact);  
      await this.getAllContacts();  
      this.$router.push("/");  
    }  
  } 
  catch (ex) {  
    console.log(ex);  
  }  
},

We need evt.preventDefault() to stop the form from submitting the normal way, i.e. without calling the Ajax code below.

this.$validator.validateAll() validates the form.

this.$validator is an object provided by Vee-Validate.

It returns a promise, so we need the function to be async, and we need await before the function call.

If result is falsy, the form validation failed, so we run return to stop the rest of the function from executing. Finally, if form fields are all valid, we can submit. Since this form is used for both adding and editing contacts, we have to check which action we’re doing. If we edit, then we call await this.updateContact(this.contact, this.contactId); to update our contact. Otherwise, we add contact so we call await this.addContact(this.contact);

In either case, we call await this.getAllContacts(); to refresh our contacts and put them in the store. If we are adding, then we will redirect to the home page at the end by calling this.$router.push(“/”); . this.updateContact , this.addContact , and this.getAllContacts are all from our contactMixin which we will write shortly.

Next, we write some helper code.

Create a folder called helpers and make a file within it called export.js — put in the following:

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" }  
]

This provides the countries that we reference in ContactForm.vue .

Next, we add our mixin to manipulate our contacts by communicating with our back end. We make a folder call mixins and create a file called contactMixin.js within it.

In the file, we put:

const axios = require('axios');  
const apiUrl = 'http://localhost:3000';

export const contactMixin = {  
    methods: {  
        getContacts() {  
            return axios.get(`${apiUrl}/contacts`);  
        }, 

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

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

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

This will let us include our functions in the methods object of the component object we include or mixin with by putting it in the mixins array of our component object.

Then we add our pages. To do this, create a views folder if it doesn’t already exists and add ContactFormPage.vue .

In there, put:

<template>  
  <div class="about">  
    <ContactForm :edit="false" />  
  </div>  
</template>

<script>  
// @ is an alias to /src  
import ContactForm from "@/components/ContactForm.vue";

export default {  
  name: "ContactFormPage",  
  components: {  
    ContactForm  
  }  
};  
</script>

This just displays the ContactForm component that we created. We set the :edit prop to false so that it’ll add our contact instead of editing.

Next, we add our home page to display a list of contacts. In the views folder, we add a file called Home.vue if it doesn’t already exist. In there we put:

<template>  
  <div class="home">  
    <div class="center">  
      <h1>Address Book Home</h1>  
    </div>  
    <md-table>  
      <md-table-row>  
        <md-table-head md-numeric>ID</md-table-head>  
        <md-table-head>First Name</md-table-head>  
        <md-table-head>Last Name</md-table-head>  
        <md-table-head>Address Line 1</md-table-head>  
        <md-table-head>Address Line 2</md-table-head>  
        <md-table-head>City</md-table-head>  
        <md-table-head>Country</md-table-head>  
        <md-table-head>Postal Code</md-table-head>  
        <md-table-head>Gender</md-table-head>  
        <md-table-head>Age</md-table-head>  
        <md-table-head>Email</md-table-head>  
        <md-table-head></md-table-head>  
        <md-table-head></md-table-head>  
      </md-table-row><md-table-row v-for="c in contacts" :key="c.id">  
        <md-table-cell md-numeric>{{c.id}}</md-table-cell>  
        <md-table-cell>{{c.firstName}}</md-table-cell>  
        <md-table-cell>{{c.lastName}}</md-table-cell>  
        <md-table-cell>{{c.addressLineOne}}</md-table-cell>  
        <md-table-cell>{{c.addressLineTwo}}</md-table-cell>  
        <md-table-cell>{{c.city}}</md-table-cell>  
        <md-table-cell>{{c.country}}</md-table-cell>  
        <md-table-cell>{{c.postalCode}}</md-table-cell>  
        <md-table-cell>{{c.gender}}</md-table-cell>  
        <md-table-cell md-numeric>{{c.age}}</md-table-cell>  
        <md-table-cell>{{c.email}}</md-table-cell>  
        <md-table-cell>  
          <md-button class="md-primary" @click="selectedContactId = c.id; showDialog = true">Edit</md-button>  
        </md-table-cell>  
        <md-table-cell>  
          <md-button class="md-accent" @click="removeContact(c.id)">Delete</md-button>  
        </md-table-cell>  
      </md-table-row>  
    </md-table><md-dialog :md-active.sync="showDialog">  
      <md-dialog-content>  
        <ContactForm  
          :editing="true"  
          :contactId="selectedContactId"  
          @contactSaved="selectedContactId = undefined; showDialog = false"  
        />  
      </md-dialog-content>  
    </md-dialog>  
  </div>  
</template>

<script>  
import { contactMixin } from "@/mixins/contactMixin";  
import ContactForm from "@/components/ContactForm.vue";

export default {  
  name: "HomePage",  
  mixins: [contactMixin],  
  components: {  
    ContactForm  
  },  
  props: {  
    editing: Boolean,  
    id: Number  
  },  
  computed: {  
    contacts() {  
      return this.$store.state.contacts;  
    }  
  },  
  data() {  
    return {  
      showDialog: false,  
      selectedContactId: undefined  
    };  
  }, 

  beforeMount() {  
    this.getAllContacts();  
  }, 

  methods: {  
    async getAllContacts() {  
      try {  
        const response = await this.getContacts();  
        this.$store.commit("setContacts", response.data);  
      } catch (ex) {  
        console.log(ex);  
      }  
    }, 

    async removeContact(id) {  
      try {  
        await this.deleteContact(id);  
        await this.getAllContacts();  
      } catch (ex) {  
        console.log(ex);  
      }  
    }  
  }  
};  
</script>

<style scoped>  
.md-dialog-container {  
  padding: 20px;  
}

.md-content.md-table.md-theme-default {  
  width: 95%;  
  margin: 0 auto;  
}  
</style>

We get our contacts during page load by call the this.getAllContacts function in the beforeMount function. Notice that we have this.getContacts function from our mixin. Mixins allows us to reuse code.

Code in our mixinx cannot have the same name as the functions in our methods objects in our components because mixin functions hooks straight into our methods, since we exported an object with methods field in our Mixin code.

In App.vue , we add our menu and top bar by putting the following:

<template>  
  <div id="app">  
    <md-toolbar class="md-accent">  
      <md-button class="md-icon-button" @click="showNavigation = true">  
        <md-icon>menu</md-icon>  
      </md-button>  
      <h3 class="md-title">Vee Validate Address Book App</h3>  
    </md-toolbar>  
    <md-drawer :md-active.sync="showNavigation" md-swipeable>  
      <md-toolbar class="md-transparent" md-elevation="0">  
        <span class="md-title">Vee Validate Address Book App</span>  
      </md-toolbar><md-list>  
        <md-list-item>  
          <router-link to="/">  
            <span class="md-list-item-text">Home</span>  
          </router-link>  
        </md-list-item><md-list-item>  
          <router-link to="/contact">  
            <span class="md-list-item-text">Add Contact</span>  
          </router-link>  
        </md-list-item>  
      </md-list>  
    </md-drawer>
    <router-view />  
  </div>  
</template>

<script>  
export default {  
  name: "app",  
  data: () => {  
    return {  
      showNavigation: false  
    };  
  }  
};  
</script>

<style lang="scss">  
.center {  
  text-align: center;  
}  
</style>

In main.js , we add our boilerplate code to include Vue Material and Vee-Validate in our app:

import Vue from 'vue'  
import App from './App.vue'  
import router from './router'  
import store from './store'  
import VueMaterial from 'vue-material'  
import 'vue-material/dist/vue-material.min.css'  
import VeeValidate from 'vee-validate';Vue.use(VeeValidate);  
Vue.use(VueMaterial);Vue.config.productionTip = falsenew Vue({  
  router,  
  store,  
  render: h => h(App)  
}).$mount('#app')

In router.js , we add our routes so we can see our pages:

import Vue from 'vue'  
import Router from 'vue-router'  
import HomePage from './views/HomePage.vue'  
import ContactFormPage from './views/ContactFormPage.vue'
Vue.use(Router)

export default new Router({  
  mode: 'history',  
  base: process.env.BASE_URL,  
  routes: [  
    {  
      path: '/',  
      name: 'home',  
      component: HomePage  
    },  
    {  
      path: '/contact',  
      name: 'contact',  
      component: ContactFormPage  
    }  
  ]  
})

In store.js , we put:

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: {}  
})

to store our contact in a place where all components can access. The store uses the Vuex library so that we have a this.$store object to call our mutation with the this.$store.commit function and get the latest data from the store via the computed property of our component object, like so:

contacts() {  
  return this.$store.state.contacts;  
}

Finally in index.html , we put:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,500,700,400italic|Material+Icons">
  <link rel="stylesheet" href="https://unpkg.com/vue-material/dist/vue-material.min.css">
  <link rel="stylesheet" href="https://unpkg.com/vue-material/dist/theme/default.css">
  <title>Address Book App</title>
</head>
<body>
  <noscript>
    <strong>We're sorry but vee-validate-tutorial-app doesn't work properly without JavaScript enabled. Please enable it
      to continue.</strong>
  </noscript>
  <div id="app"></div>
  <!-- built files will be auto injected -->
</body>
</html>

to add the Roboto font and Material icons to our app.

Now we are ready to start our JSON server. Go to our project folder and run json-server — watch db.json to start the server. It will allow us to call these routes without any configuration:

GET    /contacts  
POST   /contacts  
PUT    /contacts/1  
DELETE /contacts/1

These are all the routes we need. Data will be saved to db.json of the folder that we’re in, which should be our app’s project folder.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *