Published:

Vue.js + Vuelidate - Dynamic Form Example

Built with Vue 2.6.12 and Vuelidate 0.7.5

Other versions available:

This is a quick example of how to build a dynamic form with validation in Vue.js with Vuelidate.

Vuelidate is a lightweight model-based validation plugin for Vue.js that enables you to define validation rules on your Vue components with the validations property, and access validation error messages and Vuelidate methods via the $v object. For more info see https://vuelidate.js.org/.

The example form allows selecting the number of tickets to purchase and then entering the name and email of the person each ticket is for, both fields are required and the email field must contain a valid email address. The "Buy Tickets" button simply displays the form values in an alert popup if the form is valid, and the "Reset" button resets the form back to it's initial state including the removal of all ticket name & email fields.

Styling of the example is all done with Bootstrap 4.5 CSS, for more info see https://getbootstrap.com/docs/4.5/getting-started/introduction/.

Here it is in action: (See on CodeSandbox at https://codesandbox.io/s/vuejs-vuelidate-dynamic-form-example-844yj)


App Component with Dynamic Vuelidate Form

The app component template contains all the html markup for rendering the dynamic form. The form input fields use the v-model attribute to bind inputs to data properties in the app component. Ticket form fields are rendered dynamically by looping over the tickets array with the Vue v-for directive (v-for="(ticket, i) in tickets"), the tickets array is updated by the onChangeTickets() method which is called when the selected number of tickets is changed (@change="onChangeTickets").

Validation rules are set in the validations options of the Vue component, Vuelidate supports a bunch of validators out of the box including required and email, for a full list see https://vuelidate.js.org/#sub-builtin-validators. Validation errors are accessed in the template with the Vuelidate $v object (e.g. $v.numberOfTickets.$error), errors for each ticket are accessed via $v.tickets.$each[i] which is stored in a template variable in the v-for loop for easy access (:set="v = $v.tickets.$each[i]").

The form binds the submit event to the onSubmit() method using the Vue event binding @submit.prevent="onSubmit", .prevent prevents the default browser form submit behaviour (it's the equivalent of adding e.preventDefault() to the method). The onSubmit() method sets all the form inputs to touched ($v.touch()) to trigger the display of validation messages, then ensures the form is valid before displaying the form contents in a javascript alert.

The onReset() method resets the Vuelidate validation messages with $v.$reset(), then resets the form data back to its initial state by calling the original data() method of the Vue component (const initialData = this.$options.data.call(this)) and copying the result over the current component data (Object.assign(this.$data, initialData);).

<template>
    <form @submit.prevent="onSubmit" @reset="onReset">
        <div class="card m-3">
            <h5 class="card-header">Vue.js + Vuelidate - Dynamic Form Example</h5>
            <div class="card-body border-bottom">
                <div class="form-row">
                    <div class="form-group">
                        <label>Number of Tickets</label>
                        <select v-model="numberOfTickets" class="form-control" :class="{ 'is-invalid': $v.numberOfTickets.$error }" @change="onChangeTickets">
                            <option value=""></option>
                            <option v-for="i in [1,2,3,4,5,6,7,8,9,10]" :key="i" :value="i">{{i}}</option>
                        </select>
                        <div class="invalid-feedback">Number of tickets is required</div>
                    </div>
                </div>
            </div>
            <div v-for="(ticket, i) in tickets" :key="i" :set="v = $v.tickets.$each[i]" class="list-group list-group-flush">
                <div class="list-group-item">
                    <h5 class="card-title">Ticket {{i + 1}}</h5>
                    <div class="form-row">
                        <div class="form-group col-6">
                            <label>Name</label>
                            <input type="text" v-model="ticket.name" class="form-control" :class="{ 'is-invalid': v.name.$error }" />
                            <div class="invalid-feedback">
                                <div>Name is required</div>
                            </div>
                        </div>
                        <div class="form-group col-6">
                            <label>Email</label>
                            <input type="text" v-model="ticket.email" class="form-control" :class="{ 'is-invalid': v.email.$error }" />
                            <div class="invalid-feedback">
                                <div v-if="!v.email.required">Email is required</div>
                                <div v-if="!v.email.email">Email is invalid</div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="card-footer text-center border-top-0">
                <button class="btn btn-primary mr-1">
                    Buy Tickets
                </button>
                <button class="btn btn-secondary mr-1" type="reset">Reset</button>
            </div>
        </div>
    </form>
</template>

<script>
    import { required, email } from "vuelidate/lib/validators";

    export default {
        data() {
            return {
                numberOfTickets: '',
                tickets: []
            };
        },
        validations: {
            numberOfTickets: { required },
            tickets: {
                $each: {
                    name: { required },
                    email: { required, email }
                }
            }
        },
        methods: {
            onChangeTickets(e) {
                const numberOfTickets = Number(e.target.value || 0);
                this.tickets = [...Array(numberOfTickets).keys()].map(i => this.tickets[i] || {});
            },
            onSubmit(e) {
                // set all fields to touched
                this.$v.$touch();

                // stop here if form is invalid
                if (this.$v.$invalid) return;

                // display form values on success
                alert("SUCCESS!! :-)\n\n" + JSON.stringify(this.$data, null, 4));
            },
            onReset() {
                // reset form validation errors
                this.$v.$reset();
                
                // reset form data
                const initialData = this.$options.data.call(this);
                Object.assign(this.$data, initialData);
            }
        }
    };
</script>

 

Subscribe or Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter or GitHub to be notified when I post new content.