Vue.js + Vuelidate - Dynamic Form Example
Built with Vue 2.6.12 and Vuelidate 0.7.5
Other versions available:
- Angular: Angular 14, 10, 9, 8
- React: React Hook Form 7, 6, React + Formik
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>
Need Some Vue Help?
Search fiverr for freelance Vue developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!