Form Handling and Validation with VueJs

In this post, we will learn a step-by-step guide to create a form with VueJs. We are going to cover various steps like UI design, Form Validation, and Data Fetching through Fake API. Make sure you have installed the vue-cli in your system. The vue-cli tool will save a lot of time and effort to set up the dependencies for creating a Vue project.

Packages dependencies

  • vuelidate – Lightweight model-based validation
  • axios – Promise based HTTP client

Dev Packages dependencies

The very first step is to create your Vue project. We are going to use the Vue-CLI for creating the project.

vue create simple-form

The CLI tool will ask you with some default settings once you hit the enter after the above command. Don’t confuse and hit enter with the default settings. The project structure should be similar to the below screen.

Table of content

Start with basic UI

We are going to create the UI similar to the above screen. We are using the bootstrap 4 CSS framework. First, we start by creating the basic layout and divides our screen into 2 parts. The first and left side screen going to hold the Vue logo. And the second screen will hold our Form.

Start with App.vue

Let’s open the App.vue file from the src directory and start creating the basic layout and the left side.

<template>
  <div id="app" class="h-100">
    <div class="container-fluid h-100">
      <div class="row h-100">
        <div class="col-md-3 vue-bg h-100 d-flex justify-content-center align-items-center">
          <img alt="Vue logo" src="./assets/logo.png" width="100">
        </div>
        <div class="col-md-9 h-100 d-flex justify-content-center align-items-center">
          <div class="col-md-8 rounded px-5 py-4 shadow bg-white text-left"></div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
.vue-bg {
  background: #bce5d0;
}
</style>

The basic layout is almost ready. But why we left the right side blank? The main reason is, we are going to make a component that will handle the complete signup process.

Create SignupForm component

Let’s start by creating our component with the name SignupForm.vue at the src/components directory. And add the form and fields between the template syntax.

<template>
    <form id="signup-form">
        <div class="row">
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Full Name <span class="text-danger">*</span></label>
                <input type="text" class="form-control form-control-lg">
            </div>
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Email <span class="text-danger">*</span></label>
                <input type="email" class="form-control form-control-lg">
            </div>
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Country <span class="text-danger">*</span></label>
                <select class="form-control form-control-lg">
                    <option value="">Select Country</option>
                </select>
            </div>
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Password <span class="text-danger">*</span></label>
                <input type="password" class="form-control form-control-lg">
            </div>
            <div class="col-12 form-group text-center">
                <button class="btn btn-vue btn-lg col-4">Sign Up</button>
            </div>
        </div>
    </form>
</template>
<script>
export default {
    name: 'SignupForm'
}
</script>
<style>
.btn-vue{
    background: #53B985;
    color: #31485D;
    font-weight: bold;
}
</style>

The only thing to notice here is the name of the component that we have defined with name: 'SignupForm'. It is not mandatory to define the name of the component but yes, it is a good practice.

Now move to the App.vue file and import and locally register the newly created SignupForm module. The components options is used for local registration of our module/component.

<template>
  <div id="app" class="h-100">
    <div class="container-fluid h-100">
      <div class="row h-100">
        <div class="col-md-3 vue-bg h-100 d-flex justify-content-center align-items-center">
          <img alt="Vue logo" src="./assets/logo.png" width="100">
        </div>
        <div class="col-md-9 h-100 d-flex justify-content-center align-items-center">
          <div class="col-md-8 rounded px-5 py-4 shadow bg-white text-left">
            <SignupForm />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import SignupForm from './components/SignupForm.vue'

export default {
  name: 'App',
  components: {
    SignupForm
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
.vue-bg {
  background: #bce5d0;
}
</style>

Working with Form Validation

The UI for our form is ready and time to implement the validation to our form. We are going to use the vuelidate package for form validation.

npm i vuelidate

Now open the main.js file and import the vuelidate package. We are going to use the Vue.use() to install the vuelidate plugin for our application.

import Vue from 'vue'
import App from './App.vue'
import Vuelidate from 'vuelidate'

Vue.config.productionTip = false
Vue.use(Vuelidate);

new Vue({
  render: h => h(App),
}).$mount('#app')

Now before going to the validation section, first we need to create and map the state data with the input fields. The v-model directive is used to bind the state to the input fields.

<template>
    <form id="signup-form">
        <div class="row">
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Full Name <span class="text-danger">*</span></label>
                <input type="text" v-model.trim="fullname"  class="form-control form-control-lg">
            </div>
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Email <span class="text-danger">*</span></label>
                <input type="email" v-model.trim="email" class="form-control form-control-lg">
            </div>
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Country <span class="text-danger">*</span></label>
                <select v-model.trim="country" class="form-control form-control-lg">
                    <option value="">Select Country</option>
                </select>
            </div>
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Password <span class="text-danger">*</span></label>
                <input type="password" v-model.trim="password" class="form-control form-control-lg">
            </div>
            <div class="col-12 form-group text-center">
                <button class="btn btn-vue btn-lg col-4">Sign Up</button>
            </div>
        </div>
    </form>
</template>
<script>
export default {
    name: 'SignupForm',
    data: function() {
        return {
            fullname: '', 
            email: '', 
            country: '', 
            password: '',
            countryList: []
        }
    }
}
</script>

The next step is to import the necessary validation rules from vuelidate package and use the available validations option to validate the input fields. Also, create a submit method to handle the form submission and attach it to the form through v-on directive.

<template>
    <form id="signup-form" v-on:submit.prevent="submit">
        <div class="row">
        ....
        ....
        ....
        </div>
    </form>
</template>
<script>
import { required, email, minLength, maxLength } from 'vuelidate/lib/validators'
export default {
    name: 'SignupForm',
    data: function() {
        return {
            fullname: '', 
            email: '', 
            country: '', 
            password: ''
        }
    }, 
    validations: {
        fullname: {required},
        email: {required, email},
        country: {required},
        password: {required, minLength: minLength(6), maxLength: maxLength(18)}
    }, 
    methods: {
        submit: function() {

            this.$v.$touch();
            if (this.$v.$pendding || this.$v.$error) return;

            alert('Data Submit');
        }
    }
}
</script>

The $v is the instance of vuelidate plugin, which can be available in your application that could be a template section or the methods. The $v contains of the various methods and the params to handle the validation.

Display Validation Errors

The next step is to display the validation errors under the input fields. For this, we need to slightly modify our v-model directive. For example, the $v.fullname.$model is equal to fullname where $v.fullname.$model has to pass the validation rules.

<input type="text" v-model.trim="$v.fullname.$model" class="form-control form-control-lg">

This is not enough, we have add an additional is-invalid class to the invalid field based on the validation rules. Don’t worry, it’s not that hard. We are going to create a method validationStatus() that takes the validation and returns the boolean value based on that.

validationStatus: function(validation) {
    return typeof validation != "undefined" ? validation.$error : false;
}
<template>
    <form id="signup-form" v-on:submit.prevent="submit">
        <div class="row">
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Full Name <span class="text-danger">*</span></label>
                <input type="text" v-model.trim="$v.fullname.$model" :class="{'is-invalid': validationStatus($v.fullname)}" class="form-control form-control-lg">
                <div v-if="!$v.fullname.required" class="invalid-feedback">The full name field is required.</div>
            </div>
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Email <span class="text-danger">*</span></label>
                <input type="email" v-model.trim="$v.email.$model" :class="{'is-invalid': validationStatus($v.email)}" class="form-control form-control-lg">
                <div v-if="!$v.email.required" class="invalid-feedback">The email field is required.</div>
                <div v-if="!$v.email.email" class="invalid-feedback">The email is not valid.</div>
            </div>
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Country <span class="text-danger">*</span></label>
                <select v-model.trim="$v.country.$model" :class="{'is-invalid': validationStatus($v.country)}" class="form-control form-control-lg">
                    <option value="">Select Country</option>
                </select>
                <div v-if="!$v.country.required" class="invalid-feedback">The country field is required.</div>
            </div>
            <div class="col-12 form-group">
                <label class="col-form-label col-form-label-lg">Password <span class="text-danger">*</span></label>
                <input type="password" v-model.trim="$v.password.$model" :class="{'is-invalid': validationStatus($v.password)}" class="form-control form-control-lg">
                <div v-if="!$v.password.required" class="invalid-feedback">The password field is required.</div>
                <div v-if="!$v.password.minLength" class="invalid-feedback">You must have at least {{ $v.password.$params.minLength.min }} letters.</div>
                <div v-if="!$v.password.maxLength" class="invalid-feedback">You must not have greater then {{ $v.password.$params.maxLength.min }} letters.</div>
            </div>
            <div class="col-12 form-group text-center">
                <button class="btn btn-vue btn-lg col-4">Sign Up</button>
            </div>
        </div>
    </form>
</template>
<script>
import { required, email, minLength, maxLength } from 'vuelidate/lib/validators'
export default {
    name: 'SignupForm',
    data: function() {
        return {
            fullname: '', 
            email: '', 
            country: '', 
            password: '',
            countryList: []
        }
    }, 
    validations: {
        fullname: {required},
        email: {required, email},
        country: {required},
        password: {required, minLength: minLength(6), maxLength: maxLength(18)}
    },

    methods: {

        validationStatus: function(validation) {
            return typeof validation != "undefined" ? validation.$error : false;
        },

        submit: function() {

            this.$v.$touch();
            if (this.$v.$pendding || this.$v.$error) return;

            alert('Data Submit');
        }
    }
}
</script>

Fake REST API

We are going to use the json-server package to generate the fake REST API’s. We only required it for the countries for our form. We need to provide some data sources to the json-server package. Let’s just install and create a data.js file that holds the countries list with the original and ISO formatted name.

npm i json-server --save-dev

The data.js file should look like the below codes.

module.exports = function() {
    return {
        countries: [
            {"iso" : "AF", "country" : "Afghanistan"},
            {"iso" : "AL", "country" : "Albania"},
            .....
            .....
            .....
            .....
            .....
            {"iso" : "YE", "country" : "Yemen"},
            {"iso" : "ZM", "country" : "Zambia"},
            {"iso" : "ZW", "country" : "Zimbabwe"}
        ]
    }
}

Now create an npm script command to run the json-server with the data source and the separate port on package.json file.

{
  "name": "simple-form",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "json": "json-server src/data.js -p 4600",
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  ....
  ....
  ....
  ....
}

You could run it separately by opening another terminal or by using an additional package i.e. npm-run-all to run more than one npm scripts at once.

npm i npm-run-all --save-dev

Now configure the npm-run-all package to package.json file.

"start": "npm-run-all -p serve json"
{
  "name": "simple-form",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "json": "json-server src/data.js -p 4600",
    "serve": "vue-cli-service serve",
    "start": "npm-run-all -p serve json",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  ....
  ....
  ....
  ....
}

Now we have a new command to start our project i.e.

npm run start

Now you will get the http://localhost:4600/countries URL once you run the command npm run start to start your project.

We need an HTTP client to access the countries API endpoint provided by json-server.

npm i axios

Import and configure the axios package in the main.js file.

import Vue from 'vue'
import App from './App.vue'
import Vuelidate from 'vuelidate'
import axios from 'axios'

Vue.config.productionTip = false
Vue.use(Vuelidate);
Vue.prototype.$http = axios;

new Vue({
  render: h => h(App),
}).$mount('#app')

Go to the SignupForm component and create a lifecycle hook i.e. mounted. And add an additional state that holds all country’s data.

<script>
import { required, email, minLength, maxLength } from 'vuelidate/lib/validators'
export default {
    name: 'SignupForm',
    data: function() {
        return {
            fullname: '', 
            email: '', 
            country: '', 
            password: '',
            countryList: []
        }
    }, 
    validations: {
        fullname: {required},
        email: {required, email},
        country: {required},
        password: {required, minLength: minLength(6), maxLength: maxLength(18)}
    },

    mounted: function() {
        var v = this;
        v.$http.get(`http://localhost:4600/countries`)
        .then(function(resp) {
            v.countryList = resp.data;
        })
        .catch(function(err) {
            console.log(err)
        });
    },
    ....
    ....
    ....
}

As you see in the above codes, now we have all the countries in the countryList state. Next add the countries into the select box through v-for directive.

<div class="col-12 form-group">
    <label class="col-form-label col-form-label-lg">Country <span class="text-danger">*</span></label>
    <select v-model.trim="$v.country.$model" :class="{'is-invalid': validationStatus($v.country)}" class="form-control form-control-lg">
        <option value="">Select Country</option>
        <option :value="c.iso" :key="c.iso" v-for="c in countryList">{{ c.country }}</option>
    </select>
    <div v-if="!$v.country.required" class="invalid-feedback">The country field is required.</div>
</div>

Reset Data After Form Submission

Create another method to reset the state of our SignupForm component.

<script>
import { required, email, minLength, maxLength } from 'vuelidate/lib/validators'
export default {
    ....
    ....
    ....

    methods: {

        resetData: function() {
            this.fullname = '';
            this.email = '';
            this.country = '';
            this.password = '';
        },

        ....
        ....
        ....
        ....
        ....

        submit: function() {

            this.$v.$touch();
            if (this.$v.$pendding || this.$v.$error) return;

            alert('Data Submit');
            this.$v.$reset();
            this.resetData();
        }
    }
}
</script>

Posted by Jogesh Sharma

Jogesh Sharma is a web developer and blogger who loves all the things design and the technology, He love all the things having to do with PHP, WordPress, Joomla, Magento, Durpal, Codeigniter, jQuery, HTML5 etc. He is the author of this blog.