How to Use VeeValidate 3 for Form Validation

John Au-Yeung
Nov 6 · 11 min read

VeeValidate 3 completely 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.

Form validation errors are passed into the input from the scoped slot which is available when you add an input inside ValidationProvider . For example, if we have:

<ValidationProvider name="email" rules="required">
<div slot-scope="{ errors }">
<input v-model="name">
<p>{{ errors[0] }}</p>
</div>
</ValidationProvider>

we get errors from the ValidationProvider in the code above.

With this arrangement, specifying custom rules is easier than in version 2 as you will see in the app we will build below.

In this article, we will build a simple expense tracker with a form to add the description, amount and date of the expense and a table to display the data. We will also add buttons to let user delete expenses. In addition, there will be a page showing a line chart of the expenses sorted by date.

The back end will be built with Koa to keep it simple. We will use the latest version of Vue.js with the latest version of BootstrapVue to build the UI. Vee-Validate will be used for form validation and Vue-Chartjs will be used for the line chart.

Back End

To start, we build a simple back end to store the expenses. Create a project folder then create a backend folder to store the back end code.Now we can build the back end, we run npm init and answer the questions by entering the default values. Then we install our own packages. Koa comes with nothing, so we need to install a request body parser, a router, CORS add-on to enable cross domain requests with front end and add libraries for database.

To install all these packages, run npm i @babel/cli @babel/core @babel/node @babel/preset-env @koa/cors koa-bodyparser koa-router sequelize sqlite3 . We need the Babel packages for running with the latest JavaScript features. Sequelize and SQLite are the ORM and database that we will use respectively. The Koa packages are for enabling CORS, parsing JSON request body, and enable routing respectively.

Next run:

npx sequelize-cli init

to create database boilerplate code.

Then we run:

npx sequelize-cli --name Expense --attributes description:string,amount:float,date:date

to create a Expense table with the fields and data types listed in the attributes option.

After that is done, run npx sequelize-cli db:migrate to create the database.

Next create app.js in the root of the backend folder and add:

const Koa = require("koa");
const cors = require("@koa/cors");
const Router = require("koa-router");
const models = require("./models");
const bodyParser = require("koa-bodyparser");
const app = new Koa();
app.use(bodyParser());
app.use(cors());
const router = new Router();
router.get("/expenses", async (ctx, next) => {
const Expenses = await models.Expense.findAll();
ctx.body = Expenses;
});
router.post("/expenses", async (ctx, next) => {
const Expense = await models.Expense.create(ctx.request.body);
ctx.body = Expense;
});
router.delete("/expenses/:id", async (ctx, next) => {
const id = ctx.params.id;
await models.Expense.destroy({ where: { id } });
ctx.body = {};
});
app.use(router.routes()).use(router.allowedMethods());app.listen(3000);

This is the file with all the logic for our app. We use the Sequelize model we created by importing the models module that is created by running sequelize-cli init .

Then we enable CORS by adding app.use(cors()); JSON request body parsing is enabled by adding app.use(bodyParser()); . We add a router by adding: const router = new Router(); .

In the GET expense route, we get all the expenses. The POST is for adding a Contact. The PUT route is used for updating an existing expense by looking it up by ID. And the DELETE route is for deleting a expense by ID.

Now the back end is done. It is that simple.

Front End

To start building the front end, we add a frontend folder in the project’s root folder and then go into the frontend folder and run:

npx @vue/cli create .

When we run the wizard, we will manually select the options, and choose to include Vuex and Vue Router and use SCSS, and NPM for package management.

Next we install some packages. We will use Axios for making requests, Moment for manipulating dates, BootstrapVue for styling, Vee-Validate for form validation and Vue-Chartjs for displaying our chart.

To install everything, we run:

npm i axios bootstrap-vue chartjs vue-chartjs vee-validate moment

With all the packages installed, we can start writing code.

In the src folder, create a charts folder and create a file called ExpenseChart.vue inside it. In the file, add:

<script>
import { Line } from "vue-chartjs";
export default {
extends: Line,
props: ["chartdata", "options"],
mounted() {
this.renderChart(this.chartdata, this.options);
},
watch: {
chartdata() {
this.renderChart(this.chartdata, this.options);
},
options() {
this.renderChart(this.chartdata, this.options);
}
}
};
</script>
<style>
</style>

We specify that this component accepts the chartdata and options props. We call this.renderChart in both the mounted and watch blocks so that the chart will be updated whenever the props change or when this component first loads.

Next we create a filter to format dates. Create a filters folder in the src folder and inside it, create date.js . In the file, we add:

import * as moment from "moment";export const dateFilter = value => {
return moment(value).format("YYYY-MM-DD");
};

to take in a date and the return it formatted in the YYYY-MM-DD format.

Then we create a mixin for the HTTP request code. Create a mixins folder in the src folder and in it, create a requestsMixin.js and add:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const requestsMixin = {
methods: {
getExpenses() {
return axios.get(`${APIURL}/expenses`);
},
addExpense(data) {
return axios.post(`${APIURL}/expenses`, data);
},
deleteExpense(id) {
return axios.delete(`${APIURL}/expenses/${id}`);
}
}
};

This allows us to call these functions in any component when the mixin is included in the component.

Next in the views folder, we create our components. Create a Graph.vue file in the views folder and add:

<template>
<div class="about">
<h1 class="text-center">Expense Chart</h1>
<expense-chart :chartdata="chartData" :options="options"></expense-chart>
</div>
</template>
<script>
import { requestsMixin } from "../mixins/requestsMixin";
import * as moment from "moment";
export default {
name: "home",
mixins: [requestsMixin],
data() {
return {
chartData: {},
options: { responsive: true, maintainAspectRatio: false }
};
},
beforeMount() {
this.getAllExpenses();
},
methods: {
async getAllExpenses() {
const response = await this.getExpenses();
const sortedData = response.data.sort(
(a, b) => +moment(a.date).toDate() - +moment(b.date).toDate()
);
const dates = Array.from(
new Set(sortedData.map(d => moment(d.date).format("YYYY-MM-DD")))
);
const expensesByDate = {};
dates.forEach(d => {
expensesByDate[d] = 0;
});
dates.forEach(d => {
const data = sortedData.filter(
sd => moment(sd.date).format("YYYY-MM-DD") == d
);
expensesByDate[d] += +data
.map(a => +a.amount)
.reduce((a, b) => {
return a + b;
});
});
this.chartData = {
labels: dates,
datasets: [
{
label: "Expenses",
backgroundColor: "#f87979",
data: Object.keys(expensesByDate).map(d => expensesByDate[d])
}
]
};
}
}
};
</script>

We display the ExpenseChart that we created earlier here. The data is populated by getting them from back end. The this.getExpenses function is from the requestMixin that we included in this file. To generate the chartData , we sort the data by date, and then set the dates as the labels, and in the datasets , we have the data for the line, which is the amount of the expense. We add up all the expenses for each day and convert it into an array with:

const dates = Array.from(
new Set(sortedData.map(d => moment(d.date).format("YYYY-MM-DD")))
);
const expensesByDate = {};
dates.forEach(d => {
expensesByDate[d] = 0;
});
dates.forEach(d => {
const data = sortedData.filter(
sd => moment(sd.date).format("YYYY-MM-DD") == d
);
expensesByDate[d] += +data
.map(a => +a.amount)
.reduce((a, b) => {
return a + b;
});
});

In the code above, we convert the expenses into a dictionary with the date as the key and the total expenses as the value.

We make the chart responsive by passing in { responsive: true, maintainAspectRatio: false } to the options prop.

Next we replace the existing code in HomePage.vue with:

<template>
<div class="page">
<ValidationObserver ref="observer" v-slot="{ invalid }">
<b-form @submit.prevent="onSubmit" novalidate>
<b-form-group label="Description">
<ValidationProvider name="description" rules="required" v-slot="{ errors }">
<b-form-input
:state="errors.length == 0"
v-model="form.description"
type="text"
required
placeholder="Description"
name="description"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">Description is required</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="Amount">
<ValidationProvider name="amount" rules="required|min_value:0" v-slot="{ errors }">
<b-form-input
:state="errors.length == 0"
v-model="form.amount"
type="text"
required
placeholder="Amount"
></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="Date">
<ValidationProvider name="amount" rules="date" v-slot="{ errors }">
<b-form-input
:state="errors.length == 0"
v-model="form.date"
type="text"
required
placeholder="Date (YYYY/MM/DD)"
name="date"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">{{errors[0]}}</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-button type="submit">Add</b-button>
</b-form>
</ValidationObserver>
<b-table-simple responsive>
<b-thead>
<b-tr>
<b-th sticky-column>Description</b-th>
<b-th>Amount</b-th>
<b-th>Date</b-th>
<b-th>Delete</b-th>
</b-tr>
</b-thead>
<b-tbody>
<b-tr v-for="e in expenses" :key="e.id">
<b-th sticky-column>{{e.description}}</b-th>
<b-td>${{e.amount}}</b-td>
<b-td>{{e.date | formatDate}}</b-td>
<b-td>
<b-button @click="deleteSingleExpense(e.id)">Delete</b-button>
</b-td>
</b-tr>
</b-tbody>
</b-table-simple>
</div>
</template>
<script>
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { requestsMixin } from "../mixins/requestsMixin";
export default {
name: "home",
mixins: [requestsMixin],
data() {
return {
form: {}
};
},
beforeMount() {
this.getAllExpenses();
},
computed: {
expenses() {
return this.$store.state.expenses;
}
},
methods: {
async onSubmit() {
const isValid = await this.$refs.observer.validate();
if (!isValid) {
return;
}
await this.addExpense(this.form);
await this.getAllExpenses();
},
async getAllExpenses() {
const response = await this.getExpenses();
this.$store.commit("setExpenses", response.data);
},
async deleteSingleExpense(id) {
const response = await this.deleteExpense(id);
await this.getAllExpenses();
}
}
};
</script>

We use the form and table from BootstrapVue to build the form and table. For form validation, we wrap ValidationProvider around each b-form-input to get form validation. The form validation are rules are registered in main.js so we can use them here.

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

this.$store is provided by Vuex. We call commit on it to store the values into the Vuex store. We get the latest values from the store in the computed block.

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

<template>
<div id="app">
<b-navbar toggleable="lg" type="dark" variant="info">
<b-navbar-brand href="#">Expense Tracker</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-nav-item to="/graph" :active="path == '/graph'">Graph</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<router-view />
</div>
</template>
<script>
export default {
beforeMount() {
window.Chart.defaults.global.defaultFontFamily = `
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif`;
},
data() {
return {
path: this.$route && this.$route.path
};
},
watch: {
$route(route) {
this.path = route.path;
}
}
};
</script>
<style lang="scss">
.page {
padding: 20px;
}
</style>

In this file, we add the BootstrapVue b-navbar to display a top bar. We watch for URL changes so that we can set the correct link to be active. In the data block, we set the initial route, so that we get the correct link highlighted when the app first loads.

Next in main.js , we 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, min_value } from "vee-validate/dist/rules";
import { dateFilter } from "./filters/date";
import ExpenseChart from "./charts/ExpenseChart";extend("required", required);
extend("min_value", min_value);
extend("date", {
validate: value =>
/^(19|20)\d\d[/]([1-9]|0[1-9]|1[012])[/]([1-9]|0[1-9]|[12][0-9]|3[01])$/.test(
value
),
message: "Date must be in YYYY/MM/DD format"
});Vue.component("expense-chart", ExpenseChart);
Vue.filter("formatDate", dateFilter);
Vue.use(BootstrapVue);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.config.productionTip = false;new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");

The validation rules we used in Home.vue are added here. Note that we can have custom validation rules like the date rule. It is easy to define custom rules with VeeValidate 3. We also register the ValidationProvider and ExpenseChart components so that we can use them in our app.

We register the ValidationObserver component here to let us validate the whole form in HomePage.vue .

Also, we add the form validation rules from Vee-Validate that we want to use here. We added the built in required and min_value rules that we used in HomePage.vue here and defined the date rule below the first 2 extend calls. The date rule specifies that we check the value for the YYYY/MM/DD format and also specified the error message if validation error is found.

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

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

to include the Graph page that we created.

In store.js , we replace the code with:

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

so that we can keep the expense data in the store whenever it’s obtained.

Finally in index.html , we change the existing code to:

<!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" />
<title>Expense Tracker</title>
</head>
<body>
<noscript>
<strong
>We're sorry but frontend 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 change the title. We already imported the Bootstrap styles in App.vue so we don’t have to include it here.

After writing all that code, we can run our app. Before running anything, install nodemon by running npm i -g nodemon so that we don’t have to restart back end ourselves when files change.

Then run back end by running npm start in the backend folder and npm run servein the frontend folder.

At the end, we have the following:

JavaScript in Plain English

Learn the web's most important programming language.

John Au-Yeung

Written by

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

JavaScript in Plain English

Learn the web's most important programming language.

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