Subscribe to my email list now at http://jauyeung.net/subscribe/
Follow me on Twitter at https://twitter.com/AuMayeung
Many more articles at https://medium.com/@hohanga
VeeValidate 3 complete changed how form validation it’s done compared to the previous version. While previous versions add form validation rules straight in the input, VeeValidate 3 wraps the component provided by it around the input to provide form validation for the component. We wrap ValidationProvider
component around an input to add form validation capabilities to the input.
The built-in rules are now included with you start the app. They are all registered one by one in the entry point of the app instead of just calling Vue.use
on the library in order to use the rules.
We often need to validate form fields depending on other fields. For example, we need to validate postal code formats based on country since different countries have different postal code formats.
This is easy to do with VeeValidate 3.
In this article, we will build a Vue app that runs on Windows. It is an address book app that allows us to add contacts and save them with a back end serving a JSON file.
To start building the app, we start by installing Vue CLI by running:
npm i -g @vue/cii
Next we create our Vue.js project by running vue create address-book-app
. Be sure to select ‘Manually select features’, and after that choose to include Babel, Vuex, and Vue Router. This will create the initial files for our app.
Once we add that, we need to add our own libraries. We need Axios for making HTTP requests, Bootstrap-Vue for styling, and Vee-Validate for form validation. We install these by running:
npm i axios bootstrap-vue vee-validate
in the project folder.
Now that we installed our libraries, we can start building our address book app. We start by creating the contact form for adding and editing our contacts. We add a ContactFome.vue
file into the components
folder and add:
<template>
<ValidationObserver ref="observer" v-slot="{ invalid }">
<b-form @submit.prevent="onSubmit" novalidate>
<b-form-group label="First Name">
<ValidationProvider name="firstName" rules="required" v-slot="{ errors }">
<b-form-input
type="text"
:state="errors.length == 0"
v-model="form.firstName"
required
placeholder="First Name"
name="firstName"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">First name is requied.</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="Last Name">
<ValidationProvider name="lastName" rules="required" v-slot="{ errors }">
<b-form-input
type="text"
:state="errors.length == 0"
v-model="form.lastName"
required
placeholder="Last Name"
name="lastName"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">Last name is requied.</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="Address">
<ValidationProvider name="addressLineOne" rules="required" v-slot="{ errors }">
<b-form-input
type="text"
:state="errors.length == 0"
v-model="form.addressLineOne"
required
placeholder="Address"
name="addressLineOne"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">Address is required.</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="City">
<ValidationProvider name="city" rules="required" v-slot="{ errors }">
<b-form-input
type="text"
:state="errors.length == 0"
v-model="form.city"
required
placeholder="City"
name="city"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">City is required.</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="Postal Code">
<ValidationProvider
name="postalCode"
rules="required|postal_code:country"
v-slot="{ errors }"
>
<b-form-input
type="text"
:state="errors.length == 0"
v-model="form.postalCode"
required
placeholder="Postal Code"
name="postalCode"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">Postal code is requied.</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="Country">
<ValidationProvider name="country" rules="required" v-slot="{ errors }">
<b-form-select
:options="countries"
:state="errors.length == 0"
v-model="form.country"
required
placeholder="Country"
name="country"
></b-form-select>
<b-form-invalid-feedback :state="errors.length == 0">Country is requied.</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="Email">
<ValidationProvider name="email" rules="required|email" v-slot="{ errors }">
<b-form-input
type="text"
:state="errors.length == 0"
v-model="form.email"
required
placeholder="Email"
name="email"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="Phone">
<ValidationProvider name="phone" rules="required|phone:country" v-slot="{ errors }">
<b-form-input
type="text"
:state="errors.length == 0"
v-model="form.phone"
required
placeholder="Phone"
name="phone"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="Age">
<ValidationProvider
name="age"
rules="required|min_value:0|max_value:200"
v-slot="{ errors }"
>
<b-form-input
type="text"
:state="errors.length == 0"
v-model="form.age"
required
placeholder="Age"
name="age"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-button type="submit" variant="primary">Submit</b-button>
<b-button type="reset" variant="danger" @click="cancel()">Cancel</b-button>
</b-form>
</ValidationObserver>
</template>
<script>
import { COUNTRIES } from "@/helpers/exports";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
name: "ContactForm",
mixins: [requestsMixin],
props: {
edit: Boolean,
contact: Object
},
methods: {
async onSubmit() {
const isValid = await this.$refs.observer.validate();
if (!isValid) {
return;
}
if (this.edit) {
await this.editContact(this.form);
} else {
await this.addContact(this.form);
}
const response = await this.getContacts();
this.$store.commit("setContacts", response.data);
this.$emit("saved");
},
cancel() {
this.$emit("cancelled");
}
},
data() {
return {
form: {},
countries: COUNTRIES.map(c => ({ value: c.name, text: c.name }))
};
},
watch: {
contact: {
handler(c) {
this.form = c || {};
},
deep: true,
immediate: true
}
}
};
</script>
In the form, we wrap each input with ValidationProvider
so that we get form validation for each field, along with the form validation errors. We add :state=”errors.length == 0"
in each b-form-input
so that we get the right validation message displayed and styled properly for each input. The errors
object has the form validation error messages for each input. We also need to specify the name
prop in ValidationProvider
and b-form-input
so that form validation rules are applied to the input inside the ValidationProvider
.
We use ValidationObserver
to watch for validation errors in our form which is wrapped inside. We have the ref=”observer”
prop in the ValidationObserver
so that we can call await this.$refs.observer.validate();
to validate our form. observer
is our ref for the ValidationObserver
component. We put the form inside the ValidationObserver
component here to let us validate the whole form. With Vee-Validate, we get the this.$refs.observer.validate()
function when we use ValidationObserver
like we did in the code above. It returns a promise that resolves to true if the form is valid and false otherwise. So if it resolves to false, we don’t run the rest of the function’s code.
In this form there is cross field validation. The country field is checked before checking the phone number and postal code formats. We will add those validation rules into main.js
later.
To display the form validation error messages, we have the errors
object available in the template only. The scoped slots built into the Vee-Validate components provides the errors
object, which has the validation messages.
In the rules
prop of each field, we passing the rule names separated by pipes. The phone
and postal_code
rules are cross field rules. The country
after the colon is the name prop of the country
field, which is country
.
The form and inputs components are all provided by BootstrapVue.
When the form submit button is clicked, we call the onSubmit
button. The onSubmit
function is passed into the submit.prevent
prop to prevent the default submit action so we can use Ajax to submit the form.
In this function, we use the this.$refs.observer.validate();
to validate the form. Then after that, we call editContact
or addContact
depending if edit
prop is true or not. We passed those in from the HomePage.vue
file which we will add. These 2 functions are for making HTTP requests to our server to submit our data.
Once either of the function is called, we get the latest data and put them into our Vuex store with:
this.$store.commit("setContacts", response.data);
this.$store
is provided by Vuex.
Then we emit the saved
event to HomePage.vue
to close the modals.
The countries are imported from another file and the contact
prop is passed in from HomePage.vue
when the user selects an entry to edit.
Next create a helpers
folder in the src
folder and add an exports.js
file. In there, add:
export const COUNTRIES = [
{ name: "Afghanistan", code: "AF" },
{ name: "Aland Islands", code: "AX" },
{ name: "Albania", code: "AL" },
{ name: "Algeria", code: "DZ" },
{ name: "American Samoa", code: "AS" },
{ name: "AndorrA", code: "AD" },
{ name: "Angola", code: "AO" },
{ name: "Anguilla", code: "AI" },
{ name: "Antarctica", code: "AQ" },
{ name: "Antigua and Barbuda", code: "AG" },
{ name: "Argentina", code: "AR" },
{ name: "Armenia", code: "AM" },
{ name: "Aruba", code: "AW" },
{ name: "Australia", code: "AU" },
{ name: "Austria", code: "AT" },
{ name: "Azerbaijan", code: "AZ" },
{ name: "Bahamas", code: "BS" },
{ name: "Bahrain", code: "BH" },
{ name: "Bangladesh", code: "BD" },
{ name: "Barbados", code: "BB" },
{ name: "Belarus", code: "BY" },
{ name: "Belgium", code: "BE" },
{ name: "Belize", code: "BZ" },
{ name: "Benin", code: "BJ" },
{ name: "Bermuda", code: "BM" },
{ name: "Bhutan", code: "BT" },
{ name: "Bolivia", code: "BO" },
{ name: "Bosnia and Herzegovina", code: "BA" },
{ name: "Botswana", code: "BW" },
{ name: "Bouvet Island", code: "BV" },
{ name: "Brazil", code: "BR" },
{ name: "British Indian Ocean Territory", code: "IO" },
{ name: "Brunei Darussalam", code: "BN" },
{ name: "Bulgaria", code: "BG" },
{ name: "Burkina Faso", code: "BF" },
{ name: "Burundi", code: "BI" },
{ name: "Cambodia", code: "KH" },
{ name: "Cameroon", code: "CM" },
{ name: "Canada", code: "CA" },
{ name: "Cape Verde", code: "CV" },
{ name: "Cayman Islands", code: "KY" },
{ name: "Central African Republic", code: "CF" },
{ name: "Chad", code: "TD" },
{ name: "Chile", code: "CL" },
{ name: "China", code: "CN" },
{ name: "Christmas Island", code: "CX" },
{ name: "Cocos (Keeling) Islands", code: "CC" },
{ name: "Colombia", code: "CO" },
{ name: "Comoros", code: "KM" },
{ name: "Congo", code: "CG" },
{ name: "Congo, The Democratic Republic of the", code: "CD" },
{ name: "Cook Islands", code: "CK" },
{ name: "Costa Rica", code: "CR" },
{
name: 'Cote D"Ivoire',
code: "CI"
},
{ name: "Croatia", code: "HR" },
{ name: "Cuba", code: "CU" },
{ name: "Cyprus", code: "CY" },
{ name: "Czech Republic", code: "CZ" },
{ name: "Denmark", code: "DK" },
{ name: "Djibouti", code: "DJ" },
{ name: "Dominica", code: "DM" },
{ name: "Dominican Republic", code: "DO" },
{ name: "Ecuador", code: "EC" },
{ name: "Egypt", code: "EG" },
{ name: "El Salvador", code: "SV" },
{ name: "Equatorial Guinea", code: "GQ" },
{ name: "Eritrea", code: "ER" },
{ name: "Estonia", code: "EE" },
{ name: "Ethiopia", code: "ET" },
{ name: "Falkland Islands (Malvinas)", code: "FK" },
{ name: "Faroe Islands", code: "FO" },
{ name: "Fiji", code: "FJ" },
{ name: "Finland", code: "FI" },
{ name: "France", code: "FR" },
{ name: "French Guiana", code: "GF" },
{ name: "French Polynesia", code: "PF" },
{ name: "French Southern Territories", code: "TF" },
{ name: "Gabon", code: "GA" },
{ name: "Gambia", code: "GM" },
{ name: "Georgia", code: "GE" },
{ name: "Germany", code: "DE" },
{ name: "Ghana", code: "GH" },
{ name: "Gibraltar", code: "GI" },
{ name: "Greece", code: "GR" },
{ name: "Greenland", code: "GL" },
{ name: "Grenada", code: "GD" },
{ name: "Guadeloupe", code: "GP" },
{ name: "Guam", code: "GU" },
{ name: "Guatemala", code: "GT" },
{ name: "Guernsey", code: "GG" },
{ name: "Guinea", code: "GN" },
{ name: "Guinea-Bissau", code: "GW" },
{ name: "Guyana", code: "GY" },
{ name: "Haiti", code: "HT" },
{ name: "Heard Island and Mcdonald Islands", code: "HM" },
{ name: "Holy See (Vatican City State)", code: "VA" },
{ name: "Honduras", code: "HN" },
{ name: "Hong Kong", code: "HK" },
{ name: "Hungary", code: "HU" },
{ name: "Iceland", code: "IS" },
{ name: "India", code: "IN" },
{ name: "Indonesia", code: "ID" },
{ name: "Iran, Islamic Republic Of", code: "IR" },
{ name: "Iraq", code: "IQ" },
{ name: "Ireland", code: "IE" },
{ name: "Isle of Man", code: "IM" },
{ name: "Israel", code: "IL" },
{ name: "Italy", code: "IT" },
{ name: "Jamaica", code: "JM" },
{ name: "Japan", code: "JP" },
{ name: "Jersey", code: "JE" },
{ name: "Jordan", code: "JO" },
{ name: "Kazakhstan", code: "KZ" },
{ name: "Kenya", code: "KE" },
{ name: "Kiribati", code: "KI" },
{
name: 'Korea, Democratic People"S Republic of',
code: "KP"
},
{ name: "Korea, Republic of", code: "KR" },
{ name: "Kuwait", code: "KW" },
{ name: "Kyrgyzstan", code: "KG" },
{
name: 'Lao People"S Democratic Republic',
code: "LA"
},
{ name: "Latvia", code: "LV" },
{ name: "Lebanon", code: "LB" },
{ name: "Lesotho", code: "LS" },
{ name: "Liberia", code: "LR" },
{ name: "Libyan Arab Jamahiriya", code: "LY" },
{ name: "Liechtenstein", code: "LI" },
{ name: "Lithuania", code: "LT" },
{ name: "Luxembourg", code: "LU" },
{ name: "Macao", code: "MO" },
{ name: "Macedonia, The Former Yugoslav Republic of", code: "MK" },
{ name: "Madagascar", code: "MG" },
{ name: "Malawi", code: "MW" },
{ name: "Malaysia", code: "MY" },
{ name: "Maldives", code: "MV" },
{ name: "Mali", code: "ML" },
{ name: "Malta", code: "MT" },
{ name: "Marshall Islands", code: "MH" },
{ name: "Martinique", code: "MQ" },
{ name: "Mauritania", code: "MR" },
{ name: "Mauritius", code: "MU" },
{ name: "Mayotte", code: "YT" },
{ name: "Mexico", code: "MX" },
{ name: "Micronesia, Federated States of", code: "FM" },
{ name: "Moldova, Republic of", code: "MD" },
{ name: "Monaco", code: "MC" },
{ name: "Mongolia", code: "MN" },
{ name: "Montenegro", code: "ME" },
{ name: "Montserrat", code: "MS" },
{ name: "Morocco", code: "MA" },
{ name: "Mozambique", code: "MZ" },
{ name: "Myanmar", code: "MM" },
{ name: "Namibia", code: "NA" },
{ name: "Nauru", code: "NR" },
{ name: "Nepal", code: "NP" },
{ name: "Netherlands", code: "NL" },
{ name: "Netherlands Antilles", code: "AN" },
{ name: "New Caledonia", code: "NC" },
{ name: "New Zealand", code: "NZ" },
{ name: "Nicaragua", code: "NI" },
{ name: "Niger", code: "NE" },
{ name: "Nigeria", code: "NG" },
{ name: "Niue", code: "NU" },
{ name: "Norfolk Island", code: "NF" },
{ name: "Northern Mariana Islands", code: "MP" },
{ name: "Norway", code: "NO" },
{ name: "Oman", code: "OM" },
{ name: "Pakistan", code: "PK" },
{ name: "Palau", code: "PW" },
{ name: "Palestinian Territory, Occupied", code: "PS" },
{ name: "Panama", code: "PA" },
{ name: "Papua New Guinea", code: "PG" },
{ name: "Paraguay", code: "PY" },
{ name: "Peru", code: "PE" },
{ name: "Philippines", code: "PH" },
{ name: "Pitcairn", code: "PN" },
{ name: "Poland", code: "PL" },
{ name: "Portugal", code: "PT" },
{ name: "Puerto Rico", code: "PR" },
{ name: "Qatar", code: "QA" },
{ name: "Reunion", code: "RE" },
{ name: "Romania", code: "RO" },
{ name: "Russian Federation", code: "RU" },
{ name: "RWANDA", code: "RW" },
{ name: "Saint Helena", code: "SH" },
{ name: "Saint Kitts and Nevis", code: "KN" },
{ name: "Saint Lucia", code: "LC" },
{ name: "Saint Pierre and Miquelon", code: "PM" },
{ name: "Saint Vincent and the Grenadines", code: "VC" },
{ name: "Samoa", code: "WS" },
{ name: "San Marino", code: "SM" },
{ name: "Sao Tome and Principe", code: "ST" },
{ name: "Saudi Arabia", code: "SA" },
{ name: "Senegal", code: "SN" },
{ name: "Serbia", code: "RS" },
{ name: "Seychelles", code: "SC" },
{ name: "Sierra Leone", code: "SL" },
{ name: "Singapore", code: "SG" },
{ name: "Slovakia", code: "SK" },
{ name: "Slovenia", code: "SI" },
{ name: "Solomon Islands", code: "SB" },
{ name: "Somalia", code: "SO" },
{ name: "South Africa", code: "ZA" },
{ name: "South Georgia and the South Sandwich Islands", code: "GS" },
{ name: "Spain", code: "ES" },
{ name: "Sri Lanka", code: "LK" },
{ name: "Sudan", code: "SD" },
{ name: "Suriname", code: "SR" },
{ name: "Svalbard and Jan Mayen", code: "SJ" },
{ name: "Swaziland", code: "SZ" },
{ name: "Sweden", code: "SE" },
{ name: "Switzerland", code: "CH" },
{ name: "Syrian Arab Republic", code: "SY" },
{ name: "Taiwan, Province of China", code: "TW" },
{ name: "Tajikistan", code: "TJ" },
{ name: "Tanzania, United Republic of", code: "TZ" },
{ name: "Thailand", code: "TH" },
{ name: "Timor-Leste", code: "TL" },
{ name: "Togo", code: "TG" },
{ name: "Tokelau", code: "TK" },
{ name: "Tonga", code: "TO" },
{ name: "Trinidad and Tobago", code: "TT" },
{ name: "Tunisia", code: "TN" },
{ name: "Turkey", code: "TR" },
{ name: "Turkmenistan", code: "TM" },
{ name: "Turks and Caicos Islands", code: "TC" },
{ name: "Tuvalu", code: "TV" },
{ name: "Uganda", code: "UG" },
{ name: "Ukraine", code: "UA" },
{ name: "United Arab Emirates", code: "AE" },
{ name: "United Kingdom", code: "GB" },
{ name: "United States", code: "US" },
{ name: "United States Minor Outlying Islands", code: "UM" },
{ name: "Uruguay", code: "UY" },
{ name: "Uzbekistan", code: "UZ" },
{ name: "Vanuatu", code: "VU" },
{ name: "Venezuela", code: "VE" },
{ name: "Viet Nam", code: "VN" },
{ name: "Virgin Islands, British", code: "VG" },
{ name: "Virgin Islands, U.S.", code: "VI" },
{ name: "Wallis and Futuna", code: "WF" },
{ name: "Western Sahara", code: "EH" },
{ name: "Yemen", code: "YE" },
{ name: "Zambia", code: "ZM" },
{ name: "Zimbabwe", code: "ZW" }
];
so we can have a list of countries in the Countries field drop down in ContactForm.vue
.
Next we add the mixin that we referenced in ContactForm.vue
. Create a mixins
folder in the src
folder and add a requestsMixin.js
file. In there add:
const APIURL = "http://localhost:3000";
const axios = require("axios");
export const requestsMixin = {
methods: {
getContacts() {
return axios.get(`${APIURL}/contacts`);
},
addContact(data) {
return axios.post(`${APIURL}/contacts`, data);
},
editContact(data) {
return axios.put(`${APIURL}/contacts/${data.id}`, data);
},
deleteContact(id) {
return axios.delete(`${APIURL}/contacts/${id}`);
}
}
};
These are functions for returning promises for the requests that we make to our back end.
Next in Home.vue
, replace the existing code with the following:
<template>
<div class="page">
<h1 class="text-center">Address Book</h1>
<b-button-toolbar>
<b-button @click="openAddModal()">Add Contact</b-button>
<b-button @click="getAllContacts()">Refresh</b-button>
</b-button-toolbar>
<br />
<b-table-simple responsive>
<b-thead>
<b-tr>
<b-th>First Name</b-th>
<b-th>Last Name</b-th>
<b-th>Address</b-th>
<b-th>Phone</b-th>
<b-th>Email</b-th>
<b-th>Age</b-th>
<b-th></b-th>
<b-th></b-th>
</b-tr>
</b-thead>
<b-tbody>
<b-tr v-for="c in contacts" :key="c.id">
<b-td>{{c.firstName}}</b-td>
<b-td>{{c.lastName}}</b-td>
<b-td>{{c.addressLineOne}}, {{c.city}}, {{c.region}}, {{c.country}}, {{c.postalCode}}</b-td>
<b-td>{{c.phone}}</b-td>
<b-td>{{c.email}}</b-td>
<b-td>{{c.age}}</b-td>
<b-td>
<b-button @click="openEditModal(c)">Edit</b-button>
</b-td>
<b-td>
<b-button @click="deleteOneContact(c.id)">Delete</b-button>
</b-td>
</b-tr>
</b-tbody>
</b-table-simple>
<b-modal id="add-modal" title="Add Contact" hide-footer>
<ContactForm @saved="closeModal()" @cancelled="closeModal()" :edit="false">
</ContactForm>
</b-modal>
<b-modal id="edit-modal" title="Edit Contact" hide-footer>
<ContactForm
@saved="closeModal()"
@cancelled="closeModal()"
:edit="true"
:contact="selectedContact"
></ContactForm>
</b-modal>
</div>
</template>
<script>
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { requestsMixin } from "@/mixins/requestsMixin";
import ContactForm from "@/components/ContactForm";
export default {
name: "home",
mixins: [requestsMixin],
components: {
ContactForm
},
computed: {
contacts() {
return this.$store.state.contacts;
}
},
beforeMount() {
this.getAllContacts();
},
data() {
return {
selectedContact: {}
};
},
methods: {
openAddModal() {
this.$bvModal.show("add-modal");
},
openEditModal(contact) {
this.$bvModal.show("edit-modal");
this.selectedContact = contact;
},
closeModal() {
this.$bvModal.hide("add-modal");
this.$bvModal.hide("edit-modal");
this.selectedContact = {};
},
async deleteOneContact(id) {
await this.deleteContact(id);
this.getAllContacts();
},
async getAllContacts() {
const response = await this.getContacts();
this.$store.commit("setContacts", response.data);
}
}
};
</script>
<style scoped>
#add-button {
margin-bottom: 20px;
}
</style>
We have a table for displaying the list of contacts from the store. This component watches for our Vuex store updates by getting them from the contacts
property in the computed
field. The latest Vuex store data are always returned there.
The data is loaded when the page first loads with the getAllContacts
function call in the beforeMount
hook. getAllContacts
set the contacts in the store after it’s retrieved from back end.
We have buttons to open and close the modals which contains our contact form. Note that we have to set the form to pass into the contact
prop in the ContactForm
in the edit modal by setting this.selectedContact
in the openEditModal
with the passed in contact
argument. openEditModal
is used by the Edit button in each row of the table.
In each row of the table, there’s also a Delete button, we pass in the ID of the contact in there so that we can delete it by ID.
Next in App.vue
, we replace the existing code with:
<template>
<div id="app">
<b-navbar toggleable="lg" type="dark" variant="info">
<b-navbar-brand href="#">Address Book</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<b-nav-item to="/" :active="path == '/'">Home</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<router-view />
</div>
</template>
<script>
export default {
data() {
return {
path: this.$route && this.$route.path
};
},
watch: {
$route(route) {
this.path = route.path;
}
}
};
</script>
<style lang="scss">
.page {
padding: 20px;
}
button {
margin-right: 10px;
}
</style>
In this file, we add the BootstrapVue navbar
component and highlight the links by checking the path
which we get in the watch
block. The active
prop is the where the highlighting is set. If active
is true
then the link will be highlighted. We choose to highlight the link if the path
is equal to the route the user is in.
In the style
block, we add some padding to our page and margins to our buttons.
In main.js
replace the existing code with:
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required, email, min_value, max_value } from "vee-validate/dist/rules";
extend("required", required);
extend("email", email);
extend("min_value", min_value);
extend("max_value", max_value);
extend("phone", {
validate: (value, { country }) => {
if (["United States", "Canada"].includes(country)) {
return /^(\(\d{3}\)|\d{3})-?\d{3}-?\d{4}$/.test(value);
}
return true;
},
message: "Phone number is invalid.",
params: [{ name: "country", isTarget: true }]
});
extend("postal_code", {
validate: (value, { country }) => {
if ("United States" == country) {
return /^[0-9]{5}(?:-[0-9]{4})?$/.test(value);
} else if ("Canada" == country) {
return /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/.test(value);
}
return true;
},
message: "Phone number is invalid.",
params: [{ name: "country", isTarget: true }]
});
Vue.config.productionTip = false;
Vue.use(BootstrapVue);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
new Vue({
router,
store,
render: h => h(App),
mounted() {
this.$router.push("/");
}
}).$mount("#app");
We have the Vee-Validate validation rules added here and register our BootstrapVue components, and Vee-Validate validation components here so we can use it in our templates.
The cross-field validation rules are phone
and postal_code
. We specified the params
to be country
in the template of ContactForm.vue
, so if we specified params
to be [{ name: “country”, isTarget: true }]
, then we will get the country
field in the second argument of the validate
function. country
has the value of the country
field in ContactForm.vue
. With that, we can check for the phone number by country in the phone
validation rule.
The postal_code
rule works the same way.
Note that we have:
mounted() {
this.$router.push("/");
}
in the object passed into the Vue
constructor so that our built Windows app won’t show a blank page.
In router.js
we replace the existing code with:
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
Vue.use(Router);
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
}
]
});
to let us go to Home.vue
.
In store.js
, replace the existing code with:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
contacts: []
},
mutations: {
setContacts(state, payload) {
state.contacts = payload;
}
},
actions: {}
});
so that we can store contacts in the store for easy access by all components.
Now we can run npm run serve
to run the app.
To start the back end, we first install the json-server
package by running npm i json-server
. Then, go to our project folder and run:
json-server --watch db.json
In db.json
, change the text to:
{
"contacts": [
]
}
So we have the contacts
endpoints defined in the requests.js
available.