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. With latest versions of JavaScript
(ES2017 or later), the async
and await
keywords are introduced. It is a shorthand
for chaining promises, equivalent to calling a chain of then
functions and
returning a promise in each
of the callbacks in the then
function. For
example:
const asyncFunction = async () => {
const promise1Result = await promsise1;
console.log(promise1Result);
const promise2Result = await promsise2;
console.log(promise2Result);
}
is the same as:
const asyncFunction = () => {
promise1
.then((promise1Result) => {
console.log(promise1Result);
return promise2
})
.then((promise2Result) => {
console.log(promise2Result);
})
}
Notice that the first code block is much shorter than the second one. Each promise is called sequentially in both examples.
This syntax is available to almost all modern browsers and can be made usable with Internet Explorer with transpilers like Babel. Vue.js uses Babel so we can use it in almost any browser including Internet Explorer.
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. The package’s documentation is located at https://github.com/typicode/json-server. 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 a 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 within it, create a file 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. It is 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 is 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 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.
Similar, 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 US zip codes and Canadian postal codes.
Notice is the file above we used the async
and await
syntax for
chaining promises in this
block:
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 have at least 3 promises being called in any case,
so using the old syntax, chaining promises would be much longer than this. Also, we
can use try...catch
block to catch errors
instead of chaining the catch
function with
its own callback function passed in at the end. It is also much readable and while
it looks like synchronous code, it is not. You cannot return anything at the end but
a promise.
To display errors, we can check if errors exists for a form field, then display errors. For example, for 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 validation criteria that we specified. 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 phone, 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 is filled in in an
incorrect format and false
otherwise.
Vee-Validate provides a this.field
object to your component. So we can
check if fields are dirty, which means if 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 if they 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. Notice
that we use the async
and await
keywords for chaining promises again.
Next we write some helper code. Create a folder called
helpers
and within it, make a file called
export.js
and 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.
Next 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 exists.
Then 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 our app’s project folder.
At the end, we have the following:
async
and await
are great ways to chain promises. The
syntax is much more convenient than chaining then
functions.
The only
thing you have to worry
about is when your app has to be compatible with Internet Explorer. This syntax is
not available for Internet Explorer without something like Babel. See full
compatibility list at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function.
Subscribe to my email list now at http://jauyeung.net/subscribe/ to get more tutorials.
Follow me on Twitter at https://twitter.com/AuMayeung