Using the Vee-Validate library to enable form validation for Vue.js forms

John Au-Yeung
Sep 11 · 17 min read

Vue.js is a great framework for building front end web apps. It uses a component-based architecture which makes organizing code easy. It lets you use the latest features JavaScript has to offer, so writing code to build your apps is easier than ever. It has a lot of add-ons like routing and the flux store that you can add when you scaffold your app. However, one thing that’s missing is form validation. We have to find our own form validation library to do it, or write the code ourselves.

If we choose to use a library, Vee-Validate is a great choice, plugging directly into Vue.js form code. 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 — a great choice for Vue.js forms.

In this piece, we’ll build an address book app with Vue.js that uses Vee-Validate to validate our inputs. The form allows us to add, edit, and delete contacts.

To build our app, we first 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. The package’s documentation is located at https://github.com/typicode/json-server. When this is running, it provides us with routes to save our contact entries from front-end. To install the package, run:

npm install -g json-server

We’ll 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 need it later. Next, we have to install some libraries. We need an HTTP client, a material design library to make our app look good, and the Vee-Validate library. To get these, run npm i axios vee-validate vue-material. Axios is our HTTP client for communicating to the back-end. Vue Material is our material design library.

Next, we create the components that we nest in our page components. Create a components folder in our project folder and create a file called ContactForm.vue inside it. In this file, we put the following code:

<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': $v.firstName.$error}">
<label for="firstName">First Name</label>
<md-input name="firstName" v-model.trim="$v.firstName.$model" :disabled="sending" />
<span class="md-error" v-if="$v.firstName.$error">First Name is required.</span>
</md-field>
<br /><md-field :class="{ 'md-invalid': $v.lastName.$error }">
<label for="lastName">Last Name</label>
<md-input name="lastName" v-model.trim="$v.lastName.$model" :disabled="sending" />
<span class="md-error" v-if="$v.lastName.$error">Last Name is required.</span>
</md-field>
<br /><md-field :class="{ 'md-invalid': $v.addressLineOne.$error }">
<label for="addressLineOne">Address Line 1</label>
<md-input
name="addressLineOne"
v-model.trim="$v.addressLineOne.$model"
:disabled="sending"
/>
<span class="md-error" v-if="$v.addressLineOne.$error">Address line 1 is required.</span>
</md-field>
<br /><md-field>
<label for="addressLineTwo">Address Line 2</label>
<md-input
name="addressLineTwo"
v-model.trim="$v.addressLineTwo.$model"
:disabled="sending"
/>
</md-field>
<br /><md-field :class="{ 'md-invalid': $v.city.$error }">
<label for="city">City</label>
<md-input name="city" v-model.trim="$v.city.$model" :disabled="sending" />
<span class="md-error" v-if="$v.city.$error">City is required.</span>
</md-field>
<br /><md-field :class="{ 'md-invalid': $v.country.$error}">
<label for="country">Country</label>
<md-select name="country" v-model.trim="$v.country.$model" md-dense :disabled="sending">
<md-option :value="c" :key="c" v-for="c in countries">{{c}}</md-option>
</md-select>
<span class="md-error" v-if="$v.country.$error">Country is required.</span>
</md-field>
<br /><md-field :class="{ 'md-invalid': $v.postalCode.$error}">
<label for="postalCode">Postal Code</label>
<md-input name="postalCode" v-model.trim="$v.postalCode.$model" :disabled="sending" />
<span class="md-error" v-if="!$v.postalCode.postalCode">Postal Code is invalid.</span>
</md-field>
<br /><md-field :class="{ 'md-invalid': $v.phone.$error }">
<label for="phone">Phone</label>
<md-input name="phone" v-model.trim="$v.phone.$model" :disabled="sending" />
<span class="md-error" v-if="!$v.phone.phone">Phone is invalid.</span>
</md-field>
<br /><md-field :class="{ 'md-invalid': $v.gender.$error }">
<label for="gender">Gender</label>
<md-select name="gender" v-model.trim="$v.gender.$model" md-dense :disabled="sending">
<md-option value="male">Male</md-option>
<md-option value="female">Female</md-option>
</md-select>
<span class="md-error" v-if="$v.gender.$error ">Gender is required.</span>
</md-field>
<br /><md-field :class="{ 'md-invalid': $v.age.$error }">
<label for="age">Age</label>
<md-input
type="number"
id="age"
name="age"
autocomplete="age"
v-model.trim="$v.age.$model"
:disabled="sending"
/>
<span class="md-error" v-if="!$v.age.between">Age must be 0 and 200.</span>
</md-field>
<br />
<md-field :class="{ 'md-invalid': $v.email.$error }">
<label for="email">Email</label>
<md-input
type="email"
name="email"
autocomplete="email"
v-model.trim="$v.email.$model"
:disabled="sending"
/>
<span class="md-error" v-if="$v.email.$error">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";
import { required, between, email } from "vuelidate/lib/validators";
const postalCode = (value, vm) => {
if (vm.country == "United States") {
return /^[0-9]{5}(?:-[0-9]{4})?$/.test(value);
} else if (vm.country == "Canada") {
return /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/.test(value);
}
return true;
};
const phone = (value, vm) => {
if (["United States", "Canada"].includes(vm.country)) {
return /^[2-9]\d{2}[2-9]\d{2}\d{4}$/.test(value);
}
return true;
};
export default {
name: "ContactForm",
mixins: [contactMixin],
props: {
editing: Boolean,
contactId: Number
},
computed: {
contacts() {
return this.$store.state.contacts;
}
},
data() {
return {
sending: false,
firstName: "",
lastName: "",
age: "",
email: "",
addressLineOne: "",
addressLineTwo: "",
city: "",
country: "",
postalCode: "",
phone: "",
gender: "",
countries: COUNTRIES.map(c => c.name)
};
},
validations: {
firstName: {
required
},
lastName: {
required
},
age: {
required,
between: between(0, 200)
},
email: {
required,
email
},
addressLineOne: {
required
},
addressLineTwo: {},
city: {
required
},
country: {
required
},
postalCode: {
required,
postalCode
},
phone: {
required,
phone
},
gender: {
required
}
},
beforeMount() {
const contact = this.contacts.find(c => c.id == this.contactId) || {};
this.firstName = contact.firstName;
this.lastName = contact.lastName;
this.age = contact.age;
this.email = contact.email;
this.addressLineOne = contact.addressLineOne;
this.addressLineTwo = contact.addressLineTwo;
this.city = contact.city;
this.country = contact.country;
this.postalCode = contact.postalCode;
this.phone = contact.phone;
this.gender = contact.gender;
},
methods: {
async save(evt) {
evt.preventDefault();
try {
const contact = {
firstName: this.firstName,
lastName: this.lastName,
age: this.age,
email: this.email,
addressLineOne: this.addressLineOne,
addressLineTwo: this.addressLineTwo,
city: this.city,
country: this.country,
postalCode: this.postalCode,
phone: this.phone,
gender: this.gender
};
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
if (this.editing) {
await this.updateContact(contact, this.contactId);
await this.getAllContacts();
this.$emit("contactSaved");
} else {
await this.addContact(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);
}
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.contact-form {
margin: 0 auto;
width: 90%;
}
</style>

This contains the contact form for adding and updating contacts in our address book. It’s important that we have all the fields in the root or the dynamic validation for the phone number and postal code won’t work. So we add the following block beforeMount:

beforeMount() {
const contact = this.contacts.find(c => c.id == this.contactId) || {};
this.firstName = contact.firstName;
this.lastName = contact.lastName;
this.age = contact.age;
this.email = contact.email;
this.addressLineOne = contact.addressLineOne;
this.addressLineTwo = contact.addressLineTwo;
this.city = contact.city;
this.country = contact.country;
this.postalCode = contact.postalCode;
this.phone = contact.phone;
this.gender = contact.gender;
}

All values for each v-model.trim attribute are Vuelidate models. They’re used for validation, but the fields in data are used in the methods .

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

<span class="md-error" v-if="$v.email.$error">Email is invalid.</span>

This is for displaying validation errors for email.

We have two custom functions, for validation postal code and phone respectively:

const postalCode = (value, vm) => {
if (vm.country == "United States") {
return /^[0-9]{5}(?:-[0-9]{4})?$/.test(value);
} else if (vm.country == "Canada") {
return /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/.test(value);
}
return true;
};
const phone = (value, vm) => {
if (["United States", "Canada"].includes(vm.country)) {
return /^[2-9]\d{2}[2-9]\d{2}\d{4}$/.test(value);
}
return true;
};

They only work when the form field data are all in the top level of the object returned by thedata function. In this example, we validate the postal code and phone number by country. value has the entered data for those fields while vm has those fields not being entered.

We need evt.preventDefault() to stop the form from submitting the normal way, i.e. without calling the Ajax code below. this.$v.$touch() triggers validation. Then we call this.$v.$invalid to validate the form. These are both provided by Vuelidate. 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, 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 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 put a file in it called export.js, with the following code:

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 the contacts by communicating with our back-end. We make a folder call mixins and create a file called contactMixin.js inside it. In the file, we put the following code:

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 by putting it in the mixins array of our component object.

Next we add our pages. To do this, create a views folder, if it doesn’t already exist, and add ContactFormPage.vue. In there, put the following code:

<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 it will 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:

<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 calling the this.getAllContacts function in the beforeMount function. Notice that we have thethis.getContacts function from our mixin. Mixins allow us to reuse code. Code in our mixin cannot have the same name as the functions in our methods objects in our components — mixin functions hook 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 adding the following code:

<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">Vuelidate 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">Vuelidate 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 boilerplate code to include Vue Material and Vuelidate 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 Vuelidate from 'vuelidate'
Vue.use(Vuelidate);
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: {
}
})

This stores 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 to get the latest data from the store via the computed property of our component object, like this:

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 vuelidate-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>

This adds the Roboto font and Material icons to our app.

Now we’re 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 we’re in, which should our app’s project folder.

In the end, we have this:

Better Programming

Advice for programmers.

John Au-Yeung

Written by

Subscribe to my email list now at http://jauyeung.net/subscribe/ . Follow me on Twitter at https://twitter.com/AuMayeung

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade