Handling form errors with vuelidate in VueJS 3.0

gaisinskii

Andrey Gaisinskii

Posted on October 5, 2022

Handling form errors with vuelidate in VueJS 3.0

Intro

Hey everyone!

In today's article I will show you a clean and simple approach on handling form errors in VueJS 3.0 with vuelidate. Additionally I will be using NuxtJS 3.0 and TypeScript because they are some of my most favourite tools to use in modern frontend environment.

Motivation

Vuelidate gives us a few examples on how to display validation messages:



<!-- Display all errors -->
<p
  v-for="error of v$.$errors"
  :key="error.$uid"
>
<strong> {{ error.$message }} </strong>
</p>


Enter fullscreen mode Exit fullscreen mode

or



<!-- Display all errors of an individual property -->
<p
  v-for="error of v$.name.$errors"
  :key="error.$uid"
>
<strong> {{ error.$message }} </strong>
</p>


Enter fullscreen mode Exit fullscreen mode

And its fine if your markup is pretty simple, but what if you have a lot of inputs or you are using a ui framework that comes with an error message prop like Vuetify's textarea component does. If you are still interested then welcome aboard and lets make form validation great again!

Coding

Let's create a simple form using Vuetify 3.0 like this:



<!-- @/pages/index.vue -->

<v-form @submit.prevent="validate()" class="form">
  <v-container>
    <v-row>
      <v-col cols="12">
        <v-text-field v-model="v$.firstName.$model" label="First Name" variant="solo"/>
      </v-col>

      <v-col cols="12">
        <v-text-field v-model="v$.lastName.$model" solor label="Last Name" variant="solo" />
      </v-col>

      <v-col cols="12">
        <v-text-field v-model="v$.email.$model" label="E-mail" variant="solo"/>
      </v-col>

      <v-col cols="12">
        <v-textarea v-model="v$.message.$model" label="Message" variant="solo"/>
      </v-col>

      <v-col cols="12">
        <v-text-field v-model="v$.password.$model" label="Password" variant="solo"/>
      </v-col>

      <v-col cols="12">
        <v-btn type="submit" color="success">Submit</v-btn>
      </v-col>
    </v-row>
  </v-container>
</v-form>


Enter fullscreen mode Exit fullscreen mode

and a ref that will hold values of our inputs like this:



// @/pages/index.vue

<script setup lang="ts">
import type { IForm } from '@/interface/form.interface'

const form = ref<IForm>({
  firstName: '',
  lastName: '',
  email: '',
  message: '',
  password: ''
})
</script>


Enter fullscreen mode Exit fullscreen mode

If you are not familiar with TypeScript, IForm is an interface that helps us with type checking. If you are using JavaScript just remove the <IForm> and you are fine. For those who uses TypeScript, my interface looks like this:



// @/interface/form.interface.ts

export interface IForm {
    firstName: string;
    lastName: string;
    email: string;
    message: string;
    password: string;
}


Enter fullscreen mode Exit fullscreen mode

Now import vuelidate's validators along side with custom error messages and let's define an object with validation rules like this:



// @/pages/index.vue

<script setup lang="ts">
import { required, helpers, email } from '@vuelidate/validators'

import { FORM_INVALID_EMAIL, FORM_REQUIRED_FIELD } from '@/helpers/messages'

const rules = {
  firstName: {
    required: helpers.withMessage(FORM_REQUIRED_FIELD, required)
  },
  lastName: {
    required: helpers.withMessage(FORM_REQUIRED_FIELD, required)
  },
  email: {
    required: helpers.withMessage(FORM_REQUIRED_FIELD, required),
    email: helpers.withMessage(FORM_INVALID_EMAIL, email)
  },
  message: {
    required: helpers.withMessage(FORM_REQUIRED_FIELD, required)
  },
  password: {
    required: helpers.withMessage(FORM_REQUIRED_FIELD, required)
  }
}
</script>


Enter fullscreen mode Exit fullscreen mode


// @/helpers/messages.ts
const FORM_REQUIRED_FIELD = 'This field is required'
const FORM_INVALID_EMAIL = 'This email is invalid'

export {
  FORM_REQUIRED_FIELD,
  FORM_INVALID_EMAIL
}



Enter fullscreen mode Exit fullscreen mode

And finally let's import vuelidate itself and bind our rules and form to it:



// @/pages/index.vue
<script setup lang="ts">
import { useVuelidate } from '@vuelidate/core'

const v$ = useVuelidate(rules, form)
</script>


Enter fullscreen mode Exit fullscreen mode

To sum it up, our code will look like this:



<!-- @/pages/index.vue -->

<template>
  <main class="main">
    <v-form @submit.prevent="validate()" class="form">
      <v-container>
        <v-row>
          <v-col cols="12">
            <v-text-field v-model="v$.firstName.$model" label="First Name" variant="solo"/>
          </v-col>

          <v-col cols="12">
            <v-text-field v-model="v$.lastName.$model" solor label="Last Name" variant="solo" />
          </v-col>

          <v-col cols="12">
            <v-text-field v-model="v$.email.$model" label="E-mail" variant="solo"/>
          </v-col>

          <v-col cols="12">
            <v-textarea v-model="v$.message.$model" label="Message" variant="solo"/>
          </v-col>

          <v-col cols="12">
            <v-text-field v-model="v$.password.$model" label="Password" variant="solo"/>
          </v-col>

          <v-col cols="12">
            <v-btn type="submit" color="success">Submit</v-btn>
          </v-col>
        </v-row>
      </v-container>
    </v-form>
    </main>
</template>

<script setup lang="ts">
import { useVuelidate } from '@vuelidate/core'

import { required, helpers, email } from '@vuelidate/validators'

import { FORM_INVALID_EMAIL, FORM_REQUIRED_FIELD } from '@/helpers/messages'

import type { IForm } from '@/interface/form.interface'

const form = ref<IForm>({
  firstName: '',
  lastName: '',
  email: '',
  message: '',
  password: ''
})

const rules = {
  firstName: {
    required: helpers.withMessage(FORM_REQUIRED_FIELD, required)
  },
  lastName: {
    required: helpers.withMessage(FORM_REQUIRED_FIELD, required)
  },
  email: {
    required: helpers.withMessage(FORM_REQUIRED_FIELD, required),
    email: helpers.withMessage(FORM_INVALID_EMAIL, email)
  },
  message: {
    required: helpers.withMessage(FORM_REQUIRED_FIELD, required)
  },
  password: {
    required: helpers.withMessage(FORM_REQUIRED_FIELD, required)
  }
}

const v$ = useVuelidate(rules, form)

const validate = async () => {
    const result = await v$.value.$validate() 

    if (!result) {
        return
    }

    alert('Success! Sending to API')
}
</script>


Enter fullscreen mode Exit fullscreen mode

If you press the submit button without filling in the form and open Vue DevTools, in the v$ object you will find $errors property containing errors for each corresponding input.

Vuelidate errors

Vulidate single error

Each object in the $errors array has a $message property with a custom error message that we have defined in @/helpers/messages.ts. What we have to do here, is to iterate through $errors array and create a map of errors.

Let's create a composable useValidationErrors.ts, this function takes an array of errors from vuelidate and using reduce method creates an object of key-value pairs, where our key is a name of the validated property and our value is a custom defined message.



// @/composables/useValidationErrors.ts

import type { ErrorObject } from '@vuelidate/core'

export const useValidationErrors = <T extends Record<keyof T, string>>(errors: ErrorObject[]): Record<keyof T, string> => {
  return errors.reduce((acc, value) => {
    return { ...acc, [value.$property]: value.$message }
  }, {} as Record<keyof T, string>)
}


Enter fullscreen mode Exit fullscreen mode

JavaScript version of this composable will look like this:



// @/composables/useValidationErrors.js

export const useValidationErrors = (errors) => {
  return errors.reduce((acc, value) => {
    return { ...acc, [value.$property]: value.$message }
  }, {})
}


Enter fullscreen mode Exit fullscreen mode

Now let's wrap our composable with a computed property errors:



// @/pages/index.vue

<script setup lang="ts">
const errors = computed(() => useValidationErrors<IForm>(v$.value.$errors))
</script>


Enter fullscreen mode Exit fullscreen mode
  • Note that I haven't imported my composable anywhere, Nuxt 3.0 does that for you.

  • For JavaScript developers: just remove <IForm> from useValidationErrors

If you click on the submit button again now in the Vue DevTools you will see our errors computed property being populated.

errors computed property

If you start typing the email the error will change

errors computed property changed

Finally let's update our markup of the v-text-field component with error and error-messages props:



<v-form @submit.prevent="validate()" class="form">
      <v-container>
        <v-row>
          <v-col cols="12">
            <v-text-field
              v-model="v$.firstName.$model"
              label="First Name"
              variant="solo"
              :error="v$.firstName.$error"
              :error-messages="errors.firstName"
            />
          </v-col>

          <v-col cols="12">
            <v-text-field
              v-model="v$.lastName.$model"
              label="Last Name"
              variant="solo"
              :error="v$.lastName.$error"
              :error-messages="errors.lastName"
            />
          </v-col>

          <v-col cols="12">
            <v-text-field
              v-model="v$.email.$model"
              label="E-mail"
              variant="solo"
              :error="v$.email.$error"
              :error-messages="errors.email"
            />
          </v-col>

          <v-col cols="12">
            <v-textarea
              v-model="v$.message.$model"
              label="Message"
              variant="solo"
              :error="v$.message.$error"
              :error-messages="errors.message"
            />
          </v-col>

          <v-col cols="12">
            <v-text-field
              v-model="v$.password.$model"
              label="Password"
              variant="solo"
              :error="v$.password.$error"
              :error-messages="errors.password"
            />
          </v-col>

          <v-col cols="12">
            <v-btn
              type="submit"
              color="success"
            >
              Submit
            </v-btn>
          </v-col>
        </v-row>
      </v-container>
    </v-form>


Enter fullscreen mode Exit fullscreen mode

And voilà!

Gif of the final result is here

Nuxt 3 Minimal Starter

Look at the nuxt 3 documentation to learn more.

Setup

Make sure to install the dependencies:

# yarn
yarn install

# npm
npm install

# pnpm
pnpm install --shamefully-hoist
Enter fullscreen mode Exit fullscreen mode

Development Server

Start the development server on http://localhost:3000

npm run dev
Enter fullscreen mode Exit fullscreen mode

Production

Build the application for production:

npm run build
Enter fullscreen mode Exit fullscreen mode

Locally preview production build:

npm run preview
Enter fullscreen mode Exit fullscreen mode

Checkout the deployment documentation for more information.






Credits

I have to thank @eduardmavliutov and his colleagues for inspiring me to write this article!

💖 💪 🙅 🚩
gaisinskii
Andrey Gaisinskii

Posted on October 5, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related