All posts

Build a dynamic form component with Vue 3 (Composition API), Typescript, and Vuelidate

Portrait of Jake Bazin
May 24, 2022
5 min read
Author of the blog, Jake Bazin sitting on a sofa, relaxing with his phone.

Project set-up

This blog post assumes you have the Vue CLI installed, if you do not have it, check out the Vue documentation for how to install it. To create a Vue project, open a terminal in a directory of your choice and follow these steps:

  • Run the following command:
    • ‘vue create dynamic-form-component’ (Mac/Linux)
    • ‘winpty vue.cmd create dynamic-form-component’ (Windows)
  • Choose manually select features
  • Using the space bar select: Choose Vue version; Babel; Typescript; and Router, then hit enter
  • Choose Vue version 3.x
  • Say no to class style component syntax
  • Say yes to Babel alongside Typescript
  • Say yes to history mode for the router
  • Choose your preferred code formatter, I’m using ESLint + Prettier
  • Choose lint on save
  • Choose to have your config in dedicated files for Babel, ESLint etc
  • Finally, say no to saving the config for future projects

Once your project has been created, you will be prompted to start your dev server with the following commands:

  • Cd dynamic-form-component
  • NPM run serve

Once the dev server is running, the console will show you the url that your app is being served on. By going to that address in the web browser you will see your app which should look something like this:

vue boilerplate

Remove boilerplate code

Now that you have your project set up, remove all the boilerplate code. Firstly, in the view folder delete the About.vue file.

In the Home.vue file, replace the entire template with a heading that reads “Homepage”. Then remove the HelloWorld component that has been imported and added in the script. Your Home.vue file should now look like this:

<template><h1>Homepage</h1></template><script lang="ts">import { defineComponent } from "vue";export default defineComponent({    name: "Home",});</script>

Next in, App.vue, replace the two router-links with a router-view tag. The router-view component will attempt to match the url to a route in the routes array using the path property, and render the component specified in the matched route. For example, if you were to go to “localhost:8080” and press enter, the Home component will be rendered, because it has found a route with the path “/” and rendered the component that is specified in the object. If you go to “localhost:8080/about”, it will look for the path “/about” and render the About component. The template in your App.vue file should look like this:

<template><router-view /></template>

Inside the router folder, we can remove the “/about” route. Your routes array should now look like this:

const routes: Array<RouteRecordRaw> = [    {        path: "/",        name: "Home",        component: Home    },];

For the final part of this step, go into the components folder, and rename the file HelloWorld.vue, to something more suitable, like Form.vue. Once you have done this go into that file and delete all the code inside the template, all the code inside the “defineComponent” method, and all the CSS from inside the styles tag. This file should looks like this:

<template></template><script lang="ts">import { defineComponent } from "vue";export default defineComponent({});</script><style scoped></style>

In the next section this component will be built.

Building the form component

In this section you are going to build a dynamic form component using a data driven approach, whereby data is defined in a parent component, in our case “Home.vue”.

As this project is using Typescript, the first step is to create an interface which will define the data structure of a form field. An interface can be used as a type when defining variables and tells Typescript that the information stored in that variable must match a specific data type.

Firstly, create a folder inside the src folder and call it interfaces, then in that folder create a file named FormField.interface.ts. Note that the “.interface” part is a best practice when creating files for interfaces, but it is just a regular typescript file.

Inside this file write the following code:

export interface FormField {    name: string;    label: string;    type: string;    value: string;    error: string;}

This code defines the type “FormField” and states that this type is an object that contains the properties: name, label, type, value, and error, all of which are of type string and exports it so it can be used in other files.

Next, go into Form.vue and define the FormFields prop. Inside Form.vue, first import the interface we just defined, and PropType. Then define a prop called formFields, give it the type of object where each value is a formField and make it required.

import { defineComponent, PropType } from "vue";import FormField from "@/interfaces/FormField.interface";export default defineComponent({    props: {        formFields: {            Array as PropType<{ [key: string]: FormField }>,            required: true,        },    },});

There are two functions to define that will handle the form, one will handle the change each time a user types into an input, and the other will handle the submission of the form.

First create the setup method, this is where you will start to use the Composition API. Any variables or functions needed within the component will be defined and returned in the setup method. First, build the handleChange function.

import { defineComponent, PropType } from "vue";import FormField from "@/interfaces/FormField.interface";export default defineComponent({    props: {        formFields: {            Array as PropType<{ [key: string]: FormField }>,            required: true,        },    },    setup() {        const handleChange = (e: Event, name: string) => {            const input = e.currentTarget as HTMLInputElement;            const value = input.value;            emit("input-change", { name, value });        };        return { handleChange };});

In the code above the setup method has been created and takes props and emit as arguments. Emit is a function that can emit events and pass data to the parent component.

In the setup method, the handleChange function is defined, which takes an event and the name of the field that is being changed. Inside the function, get the element from the event and tell typescript that it is of type HTMLInputElement, if you don’t tell it the type, typescript will error and say that ‘value’ does not exist on type input. Then get the value from that element and assign it to the variable “value” and emit the name and value of the field to the parent component.

Next inside the setup method create the handleSubmit function, which for now just emits an event to the parent component. Finally, return handleChange and handleSubmit as an object.

const handleSubmit = () => emit("handle-submit");return { handleChange, handleSubmit };

Next, build the template of the Form component.

<template><form><div class="form-field" v-for="field in formFields" :key="field.name"><label :for="field.name">{{ field.label }}</label><input        v-if="field.type !== 'textarea'"        :type="field.type"        :name="field.name"        @input="handleChange($event, field.name)"      /><textarea        v-else        :name="field.name"        cols="30"        rows="10"        @input="handleChange($event, field.name)"></textarea><p class="error" v-if="field.error">{{ field.error }}</p></div><button @click="handleSubmit">Submit</button></form></template>

This markup creates a form and loops over the prop formFields. For each field inside formFields, it renders:

  • A label where the value is field.label, and sets the ‘for’ attribute to field.name.
  • An input, passing field.type to the ‘type’ attribute if field.type is not equal to textarea.
  • A text area if field.type is equal to ‘textarea’, again the type and name attributes are also from the props.
  • A p tag if field.error is truthy, i.e not an empty string.
  • And finally a button which calls handleSubmit when clicked.

Note that the input and textarea elements both have @input events which call the functions defined previously.

Here is the CSS for the component.

<style scoped>form {  width: 50%;  margin: auto;}.form-field {  display: flex;  flex-direction: column;  padding: 10px;}label {  width: 100%;  text-align: left;  margin-left: 5px;  color: rgb(139, 139, 139);}input {  border: none;  border-bottom: solid 1px #193279;  padding: 5px;  margin: 5px;}input:hover,input:focus {  border-bottom: solid 2px #193279;}.error {  text-align: left;  color: red;  margin-left: 5px;}button {  width: 25%;  margin: auto;  padding: 5px 15px;  background: #f0f0f0;  border-radius: 25px;  cursor: pointer;}button:hover {  background: #193279;  color: #fff;}</style>

Build the home component

In Home.vue you can can now add the Form component in the template underneath the h1. Pass it formFields, which will be defined shortly and listen for the events defined within the component.

<template><h1>Homepage</h1><Form    :formFields="formFields"    @input-change="handleChange"    @submit="handleSubmit"  /></template>

At the top of the script section, import the Form component, and add it to the components object. You will also need to import ref from vue. Ref is used for defining variables in the setup method that need to be reactive when their values are changed.

import { defineComponent, ref } from "vue";import Form from "@/components/Form.vue";import { FormField } from "@/interfaces/FormField.interface";export default defineComponent({    components: {        Form    },});

Here is where you create formFields using a ref. This code sets its value to an object containing some form fields. Ensure that each key in this object is the same as the name attribute within as the key will be used to match a formField with its name attribute in the handleChange function.

const formFields = ref<{ [key: string]: FormField }>({      firstName: {        name: "firstName",        label: "First name",        type: "text",        value: "",        error: "",      },      lastName: {        name: "lastName",        label: "Last name",        type: "text",        value: "",        error: "",      },      email: {        name: "email",        label: "Email address",        type: "email",        value: "",        error: "",      },      password: {        name: "password",        label: "Password",        type: "password",        value: "",        error: "",      },      confirmPassword: {        name: "confirmPassword",        label: "Confirm Password",        type: "password",        value: "",        error: "",      },    });

Next define the form handler functions:

const handleChange = (data: { name: string; value: string }) =>      (formFields.value[data.name].value = data.value);const handleSubmit = () => console.log("form submitted", formFields.value);

In this component, the handleChange function takes an object called data as a parameter, inside this data object there is a name which corresponds to the name of the field to change, and its new value. The body of this function gets the formField from the data defined above, at the index of data.name and sets it to the value that has been typed in the input box.

The handleSubmit function just logs out the data. If you are working with a backend, this is where you would make your API call.

Don’t forget to return your data and functions at the end of your setup function so that you can use it in the template.

return {    formFields,    handleChange,    handleSubmit};

Congratulations if you’ve got this far, you should now have a working form component!

The next section will cover form validation.

Adding validation

Now it’s time to add validation to the form, for this you will be using Vuelidate. Start by installing the following packages:

npm install @vuelidate/core @vuelidate/validators

The @vuelidate/core package provides the validation functionality and @vuelidate/validators provides some validation rules that can be use instead of creating your own.

Start by importing Validators and defining the validation rules in Home.vue. This form will be using maxLength, minLength, required and email, but there are more listed in the documentation if you have a specific use case: https://vuelidate.js.org/#sub-builtin-validators. Helpers is also imported which will be used to help define error messages.

import {    maxLength,    required,    minLength,    email,    helpers,} from "@vuelidate/validators";

When defining the rules make sure that the data follows the same structure as the form data defined earlier. It is the value property of each formField that needs to be validated, therefore validators must go inside a value property inside each rule. For example, firstName.value is required, so the required validator needs to be wrapped in a value property. When using helpers.withMessage, pass it the error message to displayed as the first argument and the validator as the second argument.

const rules = {    firstName: {        value: {            required,        },    },    lastName: {        value: {            required,        },    },    email: {        value: {            email: helpers.withMessage(                "Please enter a valid email address",                email            ),            required: helpers.withMessage(                "Please enter a valid email address",                required            ),        },    },    password: {        value: {            required,            minLength: helpers.withMessage(                "Password must be more than 8 characters",                minLength(8)            ),            maxLength: helpers.withMessage(                "Password must be less than 30 characters",                maxLength(30)            ),        },    },    confirmPassword: {        value: {            required,            minLength: helpers.withMessage(                "Password must be more than 8 characters",                minLength(8)            ),            maxLength: helpers.withMessage(                "Password must be less than 30 characters",                maxLength(30)            ),        },    },};

There are another two functions to write in Home.vue which will set and unset form errors. The Vuelidate object (which we will be creating soon) could be used for this, however to make it more easily accessible it can be stored in the error attribute of the form field.

const handleSetError = (data: { name: string; message: string }) =>      (formFields.value[data.name].error = data.message);const handleDeleteError = (name: string) =>      (formFields.value[name].error = "");

The first function, handleDeleteError, takes the name of a form field and sets its error attribute to an empty string, making it falsey. The second function, handleSetError, takes an object as an argument, containing the name of the field that needs an error, and the message we want to set the error to. In the body of the function, the error message gets set. Don't forget to return these methods in the setup function.

Finally, add the following data and event listeners to the Form component in the template, like so:

<Form    :formFields="formFields"    :schema="rules"    @input-change="handleChange"    @set-error="handleSetError"    @delete-error="handleDeleteError"    @submit="handleSubmit"  />

Now, back in Form.vue, import the following:

import useValidate, { ValidationArgs } from "@vuelidate/core";

Then add the prop which will accept the rules defined in the Home.vue file. Props should now look like:

props: {    formFields: {        type: Object as PropType <{ [key: string]: FormField }>,        required: true,    },    schema: {        type: Object as PropType<ValidationArgs>,        required: true,    },},

Then create the validation object and add the validation functionaility needed for our existing functions.

const v$ = useValidate(props.schema, props.formFields);

The above line defines the validation object, it takes the schema and the form fields as arguments. You can detect when a field is valid with its $touch and $validate methods.

Next create a function that validates a form field:

const validateField = (name: string) => {    const field = v$.value[name];    if (field) {        field.$touch();        if (field.$error) emit("set-error", { name, message: field.$errors[0].$message });        else emit("delete-error", name);    }};

This function takes the name of a field as an argument and attempts to find the field in the validation object and saves it to a variable. If the field exists it will use the $touch method which tells vuelidate that the field has been editted. If the field is not valid, field.$error will be true and field.$errors will contain all of the errors associated with the field. It also emits set-error if field.$error is true and passes it the name of the field and the error message, this calls the set error function in the Home.vue component. If field.$error is false it emits delete error with the name of the field which calls the delete handleDeleteError function in Home.vue.

Next call the validateField function at the end of the handleChange function so that form fields are validated on each key press.

const handleChange = (e: Event, name: string) => {      const input = e.currentTarget as HTMLInputElement;      const value = input.value;      emit("input-change", { name, value });      validateField(name);};

Then, the handeSubmit function needs some more logic. Add a new parameter which is of type ‘event’. This is to stop the page from reloading on submit by calling ‘e.preventdefault’. The $validate method checks that every field is valid. If the form is not valid, check each form field and emit set-error for every invalid field so that the appropriate errors can be set. If the form is valid, just emit submit like before.

const handleSubmit = async (e: Event) => {      e.preventDefault();      const valid = await v$.value.$validate();      if (!valid) {            const fields = Object.keys(props.formFields);            fields.forEach((fieldName) => {                if (v$.value[fieldName].$error) {                emit("set-error", {                    name: fieldName,                    message: v$.value[fieldName].$errors[0].$message,                });            }        });    } else emit("submit");};


Congratulations, you now have a working form with validation!

If you would like to see my source code, you can find it here.

Share this post
Portrait of Jake Bazin
May 24, 2022
5 min read
All posts
Author of the blog, Jake Bazin sitting on a sofa, relaxing with his phone.

Build a dynamic form component with Vue 3 (Composition API), Typescript, and Vuelidate

Project set-up

This blog post assumes you have the Vue CLI installed, if you do not have it, check out the Vue documentation for how to install it. To create a Vue project, open a terminal in a directory of your choice and follow these steps:

  • Run the following command:
    • ‘vue create dynamic-form-component’ (Mac/Linux)
    • ‘winpty vue.cmd create dynamic-form-component’ (Windows)
  • Choose manually select features
  • Using the space bar select: Choose Vue version; Babel; Typescript; and Router, then hit enter
  • Choose Vue version 3.x
  • Say no to class style component syntax
  • Say yes to Babel alongside Typescript
  • Say yes to history mode for the router
  • Choose your preferred code formatter, I’m using ESLint + Prettier
  • Choose lint on save
  • Choose to have your config in dedicated files for Babel, ESLint etc
  • Finally, say no to saving the config for future projects

Once your project has been created, you will be prompted to start your dev server with the following commands:

  • Cd dynamic-form-component
  • NPM run serve

Once the dev server is running, the console will show you the url that your app is being served on. By going to that address in the web browser you will see your app which should look something like this:

vue boilerplate

Remove boilerplate code

Now that you have your project set up, remove all the boilerplate code. Firstly, in the view folder delete the About.vue file.

In the Home.vue file, replace the entire template with a heading that reads “Homepage”. Then remove the HelloWorld component that has been imported and added in the script. Your Home.vue file should now look like this:

<template><h1>Homepage</h1></template><script lang="ts">import { defineComponent } from "vue";export default defineComponent({    name: "Home",});</script>

Next in, App.vue, replace the two router-links with a router-view tag. The router-view component will attempt to match the url to a route in the routes array using the path property, and render the component specified in the matched route. For example, if you were to go to “localhost:8080” and press enter, the Home component will be rendered, because it has found a route with the path “/” and rendered the component that is specified in the object. If you go to “localhost:8080/about”, it will look for the path “/about” and render the About component. The template in your App.vue file should look like this:

<template><router-view /></template>

Inside the router folder, we can remove the “/about” route. Your routes array should now look like this:

const routes: Array<RouteRecordRaw> = [    {        path: "/",        name: "Home",        component: Home    },];

For the final part of this step, go into the components folder, and rename the file HelloWorld.vue, to something more suitable, like Form.vue. Once you have done this go into that file and delete all the code inside the template, all the code inside the “defineComponent” method, and all the CSS from inside the styles tag. This file should looks like this:

<template></template><script lang="ts">import { defineComponent } from "vue";export default defineComponent({});</script><style scoped></style>

In the next section this component will be built.

Building the form component

In this section you are going to build a dynamic form component using a data driven approach, whereby data is defined in a parent component, in our case “Home.vue”.

As this project is using Typescript, the first step is to create an interface which will define the data structure of a form field. An interface can be used as a type when defining variables and tells Typescript that the information stored in that variable must match a specific data type.

Firstly, create a folder inside the src folder and call it interfaces, then in that folder create a file named FormField.interface.ts. Note that the “.interface” part is a best practice when creating files for interfaces, but it is just a regular typescript file.

Inside this file write the following code:

export interface FormField {    name: string;    label: string;    type: string;    value: string;    error: string;}

This code defines the type “FormField” and states that this type is an object that contains the properties: name, label, type, value, and error, all of which are of type string and exports it so it can be used in other files.

Next, go into Form.vue and define the FormFields prop. Inside Form.vue, first import the interface we just defined, and PropType. Then define a prop called formFields, give it the type of object where each value is a formField and make it required.

import { defineComponent, PropType } from "vue";import FormField from "@/interfaces/FormField.interface";export default defineComponent({    props: {        formFields: {            Array as PropType<{ [key: string]: FormField }>,            required: true,        },    },});

There are two functions to define that will handle the form, one will handle the change each time a user types into an input, and the other will handle the submission of the form.

First create the setup method, this is where you will start to use the Composition API. Any variables or functions needed within the component will be defined and returned in the setup method. First, build the handleChange function.

import { defineComponent, PropType } from "vue";import FormField from "@/interfaces/FormField.interface";export default defineComponent({    props: {        formFields: {            Array as PropType<{ [key: string]: FormField }>,            required: true,        },    },    setup() {        const handleChange = (e: Event, name: string) => {            const input = e.currentTarget as HTMLInputElement;            const value = input.value;            emit("input-change", { name, value });        };        return { handleChange };});

In the code above the setup method has been created and takes props and emit as arguments. Emit is a function that can emit events and pass data to the parent component.

In the setup method, the handleChange function is defined, which takes an event and the name of the field that is being changed. Inside the function, get the element from the event and tell typescript that it is of type HTMLInputElement, if you don’t tell it the type, typescript will error and say that ‘value’ does not exist on type input. Then get the value from that element and assign it to the variable “value” and emit the name and value of the field to the parent component.

Next inside the setup method create the handleSubmit function, which for now just emits an event to the parent component. Finally, return handleChange and handleSubmit as an object.

const handleSubmit = () => emit("handle-submit");return { handleChange, handleSubmit };

Next, build the template of the Form component.

<template><form><div class="form-field" v-for="field in formFields" :key="field.name"><label :for="field.name">{{ field.label }}</label><input        v-if="field.type !== 'textarea'"        :type="field.type"        :name="field.name"        @input="handleChange($event, field.name)"      /><textarea        v-else        :name="field.name"        cols="30"        rows="10"        @input="handleChange($event, field.name)"></textarea><p class="error" v-if="field.error">{{ field.error }}</p></div><button @click="handleSubmit">Submit</button></form></template>

This markup creates a form and loops over the prop formFields. For each field inside formFields, it renders:

  • A label where the value is field.label, and sets the ‘for’ attribute to field.name.
  • An input, passing field.type to the ‘type’ attribute if field.type is not equal to textarea.
  • A text area if field.type is equal to ‘textarea’, again the type and name attributes are also from the props.
  • A p tag if field.error is truthy, i.e not an empty string.
  • And finally a button which calls handleSubmit when clicked.

Note that the input and textarea elements both have @input events which call the functions defined previously.

Here is the CSS for the component.

<style scoped>form {  width: 50%;  margin: auto;}.form-field {  display: flex;  flex-direction: column;  padding: 10px;}label {  width: 100%;  text-align: left;  margin-left: 5px;  color: rgb(139, 139, 139);}input {  border: none;  border-bottom: solid 1px #193279;  padding: 5px;  margin: 5px;}input:hover,input:focus {  border-bottom: solid 2px #193279;}.error {  text-align: left;  color: red;  margin-left: 5px;}button {  width: 25%;  margin: auto;  padding: 5px 15px;  background: #f0f0f0;  border-radius: 25px;  cursor: pointer;}button:hover {  background: #193279;  color: #fff;}</style>

Build the home component

In Home.vue you can can now add the Form component in the template underneath the h1. Pass it formFields, which will be defined shortly and listen for the events defined within the component.

<template><h1>Homepage</h1><Form    :formFields="formFields"    @input-change="handleChange"    @submit="handleSubmit"  /></template>

At the top of the script section, import the Form component, and add it to the components object. You will also need to import ref from vue. Ref is used for defining variables in the setup method that need to be reactive when their values are changed.

import { defineComponent, ref } from "vue";import Form from "@/components/Form.vue";import { FormField } from "@/interfaces/FormField.interface";export default defineComponent({    components: {        Form    },});

Here is where you create formFields using a ref. This code sets its value to an object containing some form fields. Ensure that each key in this object is the same as the name attribute within as the key will be used to match a formField with its name attribute in the handleChange function.

const formFields = ref<{ [key: string]: FormField }>({      firstName: {        name: "firstName",        label: "First name",        type: "text",        value: "",        error: "",      },      lastName: {        name: "lastName",        label: "Last name",        type: "text",        value: "",        error: "",      },      email: {        name: "email",        label: "Email address",        type: "email",        value: "",        error: "",      },      password: {        name: "password",        label: "Password",        type: "password",        value: "",        error: "",      },      confirmPassword: {        name: "confirmPassword",        label: "Confirm Password",        type: "password",        value: "",        error: "",      },    });

Next define the form handler functions:

const handleChange = (data: { name: string; value: string }) =>      (formFields.value[data.name].value = data.value);const handleSubmit = () => console.log("form submitted", formFields.value);

In this component, the handleChange function takes an object called data as a parameter, inside this data object there is a name which corresponds to the name of the field to change, and its new value. The body of this function gets the formField from the data defined above, at the index of data.name and sets it to the value that has been typed in the input box.

The handleSubmit function just logs out the data. If you are working with a backend, this is where you would make your API call.

Don’t forget to return your data and functions at the end of your setup function so that you can use it in the template.

return {    formFields,    handleChange,    handleSubmit};

Congratulations if you’ve got this far, you should now have a working form component!

The next section will cover form validation.

Adding validation

Now it’s time to add validation to the form, for this you will be using Vuelidate. Start by installing the following packages:

npm install @vuelidate/core @vuelidate/validators

The @vuelidate/core package provides the validation functionality and @vuelidate/validators provides some validation rules that can be use instead of creating your own.

Start by importing Validators and defining the validation rules in Home.vue. This form will be using maxLength, minLength, required and email, but there are more listed in the documentation if you have a specific use case: https://vuelidate.js.org/#sub-builtin-validators. Helpers is also imported which will be used to help define error messages.

import {    maxLength,    required,    minLength,    email,    helpers,} from "@vuelidate/validators";

When defining the rules make sure that the data follows the same structure as the form data defined earlier. It is the value property of each formField that needs to be validated, therefore validators must go inside a value property inside each rule. For example, firstName.value is required, so the required validator needs to be wrapped in a value property. When using helpers.withMessage, pass it the error message to displayed as the first argument and the validator as the second argument.

const rules = {    firstName: {        value: {            required,        },    },    lastName: {        value: {            required,        },    },    email: {        value: {            email: helpers.withMessage(                "Please enter a valid email address",                email            ),            required: helpers.withMessage(                "Please enter a valid email address",                required            ),        },    },    password: {        value: {            required,            minLength: helpers.withMessage(                "Password must be more than 8 characters",                minLength(8)            ),            maxLength: helpers.withMessage(                "Password must be less than 30 characters",                maxLength(30)            ),        },    },    confirmPassword: {        value: {            required,            minLength: helpers.withMessage(                "Password must be more than 8 characters",                minLength(8)            ),            maxLength: helpers.withMessage(                "Password must be less than 30 characters",                maxLength(30)            ),        },    },};

There are another two functions to write in Home.vue which will set and unset form errors. The Vuelidate object (which we will be creating soon) could be used for this, however to make it more easily accessible it can be stored in the error attribute of the form field.

const handleSetError = (data: { name: string; message: string }) =>      (formFields.value[data.name].error = data.message);const handleDeleteError = (name: string) =>      (formFields.value[name].error = "");

The first function, handleDeleteError, takes the name of a form field and sets its error attribute to an empty string, making it falsey. The second function, handleSetError, takes an object as an argument, containing the name of the field that needs an error, and the message we want to set the error to. In the body of the function, the error message gets set. Don't forget to return these methods in the setup function.

Finally, add the following data and event listeners to the Form component in the template, like so:

<Form    :formFields="formFields"    :schema="rules"    @input-change="handleChange"    @set-error="handleSetError"    @delete-error="handleDeleteError"    @submit="handleSubmit"  />

Now, back in Form.vue, import the following:

import useValidate, { ValidationArgs } from "@vuelidate/core";

Then add the prop which will accept the rules defined in the Home.vue file. Props should now look like:

props: {    formFields: {        type: Object as PropType <{ [key: string]: FormField }>,        required: true,    },    schema: {        type: Object as PropType<ValidationArgs>,        required: true,    },},

Then create the validation object and add the validation functionaility needed for our existing functions.

const v$ = useValidate(props.schema, props.formFields);

The above line defines the validation object, it takes the schema and the form fields as arguments. You can detect when a field is valid with its $touch and $validate methods.

Next create a function that validates a form field:

const validateField = (name: string) => {    const field = v$.value[name];    if (field) {        field.$touch();        if (field.$error) emit("set-error", { name, message: field.$errors[0].$message });        else emit("delete-error", name);    }};

This function takes the name of a field as an argument and attempts to find the field in the validation object and saves it to a variable. If the field exists it will use the $touch method which tells vuelidate that the field has been editted. If the field is not valid, field.$error will be true and field.$errors will contain all of the errors associated with the field. It also emits set-error if field.$error is true and passes it the name of the field and the error message, this calls the set error function in the Home.vue component. If field.$error is false it emits delete error with the name of the field which calls the delete handleDeleteError function in Home.vue.

Next call the validateField function at the end of the handleChange function so that form fields are validated on each key press.

const handleChange = (e: Event, name: string) => {      const input = e.currentTarget as HTMLInputElement;      const value = input.value;      emit("input-change", { name, value });      validateField(name);};

Then, the handeSubmit function needs some more logic. Add a new parameter which is of type ‘event’. This is to stop the page from reloading on submit by calling ‘e.preventdefault’. The $validate method checks that every field is valid. If the form is not valid, check each form field and emit set-error for every invalid field so that the appropriate errors can be set. If the form is valid, just emit submit like before.

const handleSubmit = async (e: Event) => {      e.preventDefault();      const valid = await v$.value.$validate();      if (!valid) {            const fields = Object.keys(props.formFields);            fields.forEach((fieldName) => {                if (v$.value[fieldName].$error) {                emit("set-error", {                    name: fieldName,                    message: v$.value[fieldName].$errors[0].$message,                });            }        });    } else emit("submit");};


Congratulations, you now have a working form with validation!

If you would like to see my source code, you can find it here.

Download
To download the assets, please enter your details below:
By completing this form, you provide your consent to our processing of your information in accordance with Leighton's privacy policy.

Thank you!

Use the button below to download the file. By doing so, the file will open in a separate browser window.
Download now
Oops! Something went wrong while submitting the form.
By clicking “Accept All Cookies”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.