Vue JS 3 Composition API → Registration Form Validation [2023]

Last modified on May 31st, 2023
Raja Tamil
Vue.js

Want to learn how to implement client-side form validation functionality in Vue js 3 Composition API for the sign-up and login pages?

Then, you’ve come to the right place!

Before going any further, let’s take a look at what you’re going to learn in this Vue JS 3 Composition API tutorial.

As you can see from the final output below, all of the input fields in the sign-up form have empty check validation as well as additional validation such as email, min length etc on keyup and blur events.

When all the input fields are filled with no errors, the submit button is enabled, otherwise it is disabled.

Along the way, you’re going to learn how to create reusable and scalable code for form validation that you can use throughout your web app.

Sound interesting? Let’s get started!

I assume you already know how to get Up and Running With Vue JS 3 Project Using Vue CLI

If you’re using Options API instead of Composition API, check out the link below:
Vue JS 2 Form Validation Using Options API

Sign-Up Form Vue Component

I have a simple SignUp.vue component inside the src/views folder and it has a basic skeleton of a Composition API vue component.

Some CSS style at the bottom makes the form center to the viewport horizontally and vertically.

Signup.vue

<template>
  <section class="signup-view">
    <form class="ui form">
    </form>
  </section>
</template>
<script>
export default {
  setup() {},
};
</script>
<style scoped>
.signup-view {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}
.form {
  width: 450px;
}
</style>

Input Field Child Component

Rather than creating template code for the name input field in the SignUp.vue file, create a new child component called NameField.vue inside the src/components folder.

NameField.vue

<template>
  <div class="field">
    <div class="ui left icon input big">
      <i class="user icon"></i>
      <input type="text" placeholder="Full Name" autocomplete="off" />
    </div>
  </div>
</template>
 <script>
export default {
  setup() {},
};
</script>

As you can see, I’ve used some additional CSS classes that are coming from Semantic UI. You do not need to know this in order to follow along with the rest of this guide.

Feel free to use your favourite CSS framework.

Import Child Component To SignUp.vue

Let’s import the NameField.vue child component to the Signup.vue with three simple steps.

1 import NameField.vue child components inside the Signup.vue file.

Signup.vue

import NameField from "@/components/NameField";

2 Register it by adding it to the components object.

Signup.vue

export default {
 components: {
   NameField,
 },
 ...
}

3 Finally, call it in the template.

Signup.vue

<NameField />

And the output will look like this:

Pretty straight forward!

Input Field Validation Using Computed Property

Now, we’re ready to do the validation for the Name Input Field.

First, I’m going to check if the input field is empty or not using computed property inside the NameField.vue component.

Declare a variable called input initialized with an empty string using ref() object inside the setup() function.

Recommended
Differences Between Ref() Vs Reactive() in Vue 3 Compoisition API

Bind it to the name input field at the top in the vue template.

Now, define a computed property called error which will return an error message if the value of the input field is empty, otherwise return an empty string.

NameField.vue

<template>
  ...
  <input
    type="text"
    placeholder="Full Name"
    autocomplete="off"
    v-model="input"
  />
  ...
</template>
<script>
import { ref, computed } from "vue";
export default {
  setup() {
    let input = ref("");
    const error = computed(() => {
      return input.value === "" ? "The Input field is required" : “”;
    });
    return { input, error };
  },
};
</script>

Finally, add the error variable to the returned object to show the error message on the view.

Display The Validation Error Message

Now, let add some markup to show the error message.

As I mentioned earlier, the additional CSS classes in the markup come from the Semantic UI CSS Framework.

Also check to make sure to only show the error message if the error variable is not empty using the v-if directive.

NameField.vue

<template>
  <div class="field">
    <div class="ui left icon input big">
      <i class="user icon"></i>
      <input type="text" placeholder="Full Name" autocomplete="off" />
    </div>
    <div class="ui basic label pointing red" v-if="error">
     {{ error }}
    </div>
  </div>
</template>

This works fine…

But the error message is visible by default…

What we want is to not show any error until the user starts interacting with the input field.

To fix it, set the default value of the input variable to null instead of “” .

NameField.vue

let input = ref(null);

That’s works great!

So what’s happening…? 🤷‍♀️

When the component loads, the value of the input is set to null so the input.value === “” inside the error computed property is false which hides the error message by default.

When a user starts typing and clearing all of the characters, the error computed property returns the error message which then will be visible to a user.

Here is another problem…🛑

What if a user clicks the input field and clicks somewhere aka blur?

Well…

We need to show the error in that scenario as well for a better user experience.

Basically, we want to show the error message when a user starts typing and clears all the characters out or clicks it and clicks somewhere else.

In other words, we want to show the error message when the input field is empty on keyup and blur events.

Let’s see how to do that next.

Recommended
Search Bar Using Computed Properties in Vue 3 Compoisition API

Input Field Validation Using Keyup & Blur Events

Instead of the error computed property, attach a keyup event to the name input field inside the NameField.vue component.

NameField.vue

<input
    type="text"
    placeholder="Full Name"
    autocomplete="off"
    v-model="input"
    @keyup="validateInput"
/>

Declare validateInput function, do the validation check inside it and assign the error message to the error variable which is declared above using ref() this time.

NameField.vue

<script>
import { ref } from "vue";
export default {
 setup() {
   let input = ref(“”);
   let error = ref(“”);
   const validateInput = () => {
     error.value = input.value === "" ? "The Input field is required" : "";
   };
   return { input, error, validateInput };
 },
};
</script>

This gives the same result like before.

So what’s the big deal?

Now I can attach a blur event to the input field calling the same validateInput callback function.

NameField.vue

<input
       type="text"
       placeholder="Full Name"
       autocomplete="off"
       v-model="input"
       @keyup="validateInput"
       @blue="validateInput"
/>

As you can see, the error message is not only showing when the user clears all the characters from the input field, but also when the input field is clicked and unfocused.

Create Reusable Form Validation Module

I could keep the validation code in the NameField.vue component but it’s not scalable / reusable.

Instead, move the validation code to its own function.

So, create a file called useFormValidation.js file inside the src/modules folder.

In there, declare errors variable initialized with an empty object using reactive().

useFormValidation.js

import { reactive } from "@vue/reactivity";
const errors = reactive({});

export default function useFormValidation() {
    return { errors }
}

Make sure the errors variable is declared outside of the useFormValidation function.

We’ll be accumulating errors from different input fields later in this article for managing the sign-up button state.

Validate Input Field Using Form Validation Module

Define a function called validateNameField() inside the useFormValidation() function, which takes two arguments: fieldName and fieldValue.

I’ll be calling this from the NameField.vue component in just a moment.

Check to see if the fieldValue is empty…if yes, create a property with the fieldName and, in this case, name inside the errors object.

useFormValidation.js

import { reactive } from "@vue/reactivity";
const errors = reactive({});
export default function useFormValidation() {
    const validateNameField = (fieldName, fieldValue) => {
        errors[fieldName] = fieldValue === "" ? "The " + fieldName + " field is required" : "";
    }
    return { errors, validateNameField }
}

Finally, add the validateNameField() function to the returned object.

Let’s use this function inside the NameField.vue component with THREE steps.

1 Import useFormValidation inside the NameField.vue file.

2. Call the useFormValdiation() function inside the setup() function on the right and get the validateNameField() function and errors variable by destructuring the returned object on the left.

3. Finally, call the validateNameField() function inside the validateInput() callback function with the first parameter fieldName…in this case name.

The second parameter fieldValue in this case is the value of the name input field.

NameField.vue

<script>
import { ref } from "vue";
import useFormValidation from "@/modules/useFormValidation";
export default {
  setup() {
    let input = ref("");
    const { validateNameField, errors } = useFormValidation();
    const validateInput = () => {
      validateNameField("name", input.value);
    };
    return { input, error, validateInput };
  },
};
</script>

Finally, update the error message in the NameField.vue template.

NameField.vue

<template>
  <div class="field">
    ...
    <div class="ui basic label pointing red" v-if="error.name">
     {{ error.name }}
    </div>
  </div>
</template>

Create Validator functions

As we know, we’ll be doing the empty validation check to the remaining input fields later in this article.

To handle that, I’m going to create a new file called Validators.js module inside the src/modules folder.

This is where I’ll be declaring all the validator functions that can be utilized more than once.

In there, declare the isEmpty() function, which will return an error message if the fieldValue is empty otherwise return an empty string.

Validators.js

export default function useValidators() {
   const isEmpty = (fieldName, fieldValue) => {
    return !fieldValue ? "The " + fieldName + " field is required" : "";
   }
   const minLength = (fieldName, fieldValue, min) => {
        return fieldValue.length < min ? `The ${fieldName} field must be atleast ${min} characters long` : "";
  }
  return {isEmpty, minLength}
}

Next, declare another function called minLength(), which takes a third parameter with any flexible minimum length number to validate the character length of the input data.

Finally, add both functions to the returned object.

Input Field Multiple Validation

Let’s use these functions inside the validateNameField() function inside the useFormValidation.js file.

1. Import the validators.js file at the top of the useFormValidation.js file.

import useValidators from '@/modules/validators'

2. Destructure the functions by calling useValidator().

const { isEmpty, minLength } = useValidators();

3. Use these functions inside the validateNameField() function.

const validateNameField = (fieldName, fieldValue) => {
   errors[fieldName] = !fieldValue ? isEmpty(fieldName, fieldValue) : minLength(fieldName, fieldValue, 4)
}

And the output will look like this:

Email Field Validation

We’ve done the base skeleton for validation, so it’ll be easier to validate the rest of the input fields. 😀

Let’s create an EmailField.vue child component inside the src/components folder and add the following code:

EmailField.vue

<template>
  <div class="field">
    <div class="ui left icon input big">
      <i class="email icon"></i>
      <input
        type="email"
        placeholder="Email"
        autocomplete="off"
        v-model="input"
        @keyup="validateInput"
        @blur="validateInput"
      />
    </div>
    <div class="ui basic label pointing red" v-if="errors.email">
      {{ errors.email }}
    </div>
  </div>
</template>
 <script>
import { ref } from "vue";
import useFormValidation from "@/modules/useFormValidation";
export default {
  setup() {
    let input = ref("");
    const { validateEmailField, errors } = useFormValidation();
    const validateInput = () => {
      validateEmailField("email", input.value);
    };
    return { input, errors, validateInput };
  },
};
</script>

Now, I want to do the empty field check using the isEmpty() function that I’ve already declared inside the validators.js file.

Addtionally, I’m going to check to see if the input is a valid email using isEmail().

Go to validators.js and declare the isEmail() function, which of course returns an error message when an invalid email is detected.

validators.js


export default function useValidators() {

    const isEmpty = (fieldName, fieldValue) => {
        return !fieldValue ? "The " + fieldName + " field is required" : "";
    }

    const minLength = (fieldName, fieldValue, min) => {
        return fieldValue.length < min ? `The ${fieldName} field must be atleast ${min} characters long` : "";
    }

    const isEmail = (fieldName, fieldValue) => {
        let re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
        return !re.test(fieldValue) ? "The input is not a valid " + fieldName + " address" : "";
    }
    return { isEmpty, minLength, isEmail }
}

Let’s utilize the isEmail() function inside the validateEmailField() function.

useFormValidation.js

import { reactive } from "@vue/reactivity";
const errors = reactive({});
import useValidators from '@/modules/validators'

export default function useFormValidation() {
    const { isEmpty, minLength, isEmail } = useValidators();
    const validateNameField = (fieldName, fieldValue) => {
        errors[fieldName] = !fieldValue ? isEmpty(fieldName, fieldValue) : minLength(fieldName, fieldValue, 4)
    }
    const validateEmailField = (fieldName, fieldValue) => {
        errors[fieldName] = !fieldValue ? isEmpty(fieldName, fieldValue) : isEmail(fieldName, fieldValue)
    }
    return { errors, validateNameField, validateEmailField }
}

The last step is to add the EmailField.vue component to the Signup.vue parent component.

Signup.vue

<template>
  <section class="signup-view">
    <form class="ui form" @submit.prevent novalidate>
      <NameField />
      <EmailField />
    </form>
  </section>
</template>

<script>
import NameField from "@/components/NameField";
import EmailField from "@/components/EmailField";

export default {
  components: {
    NameField,
    EmailField,
  },
};
</script>

Phone Field Validation

For the phone input field, I’m going to check to see if the typed characters are numbers in addition to the empty field check.

PhoneField.vue

<template>
  <div class="field">
    <div class="ui left icon input big">
      <i class="phone icon"></i>

      <input
        type="text"
        placeholder="Phone"
        autocomplete="off"
        v-model="input"
        @keyup="validateInput"
        @blur="validateInput"
      />
    </div>
    <div class="ui basic label pointing red" v-if="errors.phone">
      {{ errors.phone }}
    </div>
  </div>
</template>

 <script>
import { ref } from "vue";
import useFormValidation from "@/modules/useFormValidation";
export default {
  setup() {
    let input = ref("");
    const { validatePhoneField, errors } = useFormValidation();
    const validateInput = () => {
      validatePhoneField("phone", input.value);
    };
    return { input, errors, validateInput };
  },
};
</script>

Declare the validatePhoneField() function inside the useFormValidation().

const validatePhoneField = (fieldName, fieldValue) => {
        errors[fieldName] = !fieldValue ? isEmpty(fieldName, fieldValue) : isNum(fieldName, fieldValue)
}

Now, let’s declare the isNum() function inside the validator.js file, which will check if there are only numbers in the phone input field.

const isNum = (fieldName, fieldValue) => {
    let isNum = /^\d+$/.test(fieldValue);
    return !isNum ? "The " + fieldName + " field only have numbers" : "";
}

Lastly, add the PhoneField.vue component to the Signup.vue file.

<template>
  <section class="signup-view">
    <form class="ui form" @submit.prevent novalidate>
      <NameField />
      <EmailField />
      <PhoneField />
    </form>
  </section>
</template>

<script>
import NameField from "@/components/NameField";
import EmailField from "@/components/EmailField";
import PhoneField from "@/components/PhoneField";

export default {
  components: {
    NameField,
    EmailField,
    PhoneField,
  },
};
</script>

Password Field Validation

For the password, I’m going to check if the password characters length is at least 8 characters long in addition to the empty input field check.

PasswordField.vue

<template>
  <div class="field">
    <div class="ui left icon input big">
      <i class="lock icon"></i>

      <input
        type="password"
        placeholder="Password"
        autocomplete="off"
        v-model="input"
        @keyup="validateInput"
        @blur="validateInput"
      />
    </div>
    <div class="ui basic label pointing red" v-if="errors.password">
      {{ errors.password }}
    </div>
  </div>
</template>

 <script>
import { ref } from "vue";
import useFormValidation from "@/modules/useFormValidation";
export default {
  setup() {
    let input = ref("");
    const { validatePasswordField, errors } = useFormValidation();
    const validateInput = () => {
      validatePasswordField("password", input.value);
    };
    return { input, errors, validateInput };
  },
};
</script>

Declare the validatePasswordField() function inside the useFormValidation().

const validatePasswordField = (fieldName, fieldValue) => {
        errors[fieldName] = !fieldValue ? isEmpty(fieldName, fieldValue) : minLength(fieldName, fieldValue, 8)
}

Luckily, I can reuse the minLength() function, passing different minimum length value…in this case 8.

Lastly, add the PasswordField.vue component to the Signup.vue file.

<template>
  <section class="signup-view">
    <form class="ui form" @submit.prevent novalidate>
      <NameField />
      <EmailField />
      <PhoneField />
      <PasswordField />
    </form>
  </section>
</template>

<script>
import NameField from "@/components/NameField";
import EmailField from "@/components/EmailField";
import PhoneField from "@/components/PhoneField";
import PasswordField from "@/components/PasswordField";

export default {
  components: {
    NameField,
    EmailField,
    PhoneField,
    PasswordField
  },
};
</script>

Sign Up Button State Validation

Let’s disable the sign-up button by default and enable it when all of the input fields have valid data.

Add a button element to the template right below the password field in the Signup.vue file.

<button class="ui button red fluid big">
    SIGN UP
</button>

Prevent default form submission by adding @prevent modifier to the form tag.

<form @submit.prevent class="ui form">
...
</form>

There are multiple ways to handle this. For more:
Prevent Default Form Submission in Vue.js

When all of the input fields are filled and have valid data, then enable the sign-up button, otherwise, disable it.

Create Reusable Submit Button State Module

To make the button state reusable, I’m going to create a separate module file called useSubmitButtonState.js inside the src/modules folder.

The useSubmitButtonState() takes two parameters:

  • user object
  • errors object

Inside the function, create a computed property called isSignupButtonDisabled.

UseSubmitButtonState.js

import { computed } from "vue";

export default function useSubmitButtonState(user, errors) {

    const isSignupButtonDisabled = computed(() => {
        let disabled = true;
        for (let prop in user) {
            if (!user[prop] || errors[prop]) {
                disabled = true;
                break;
            }
            disabled = false;
        }
        return disabled;
    });

    return { isSignupButtonDisabled }
}

It loops through the user object (that I’ll be creating in just a moment inside Signup.vue) and checks if any of its properties are empty or if any of the same properties in the errors object have error messages.

In those cases, disabled the submit button, otherwise, enable it.

In order to utilize the above function inside the Signup.vue file, we need to create a user object and bind its properties to the appropriate input fields.

For the errors object, we can easily get it by calling the useFormValidation() function.

Create User Object

Inside the setup() function in the Signup.vue file, declare the user object with FOUR properties:

  • name,
  • email
  • phone, and
  • password

Add it to the returned object.

Bind them to the appropriate input child components using the v-model directive.

SignUp.vue

<template>
  <section class="signup-view">
    <form class="ui form" @submit.prevent>
      <NameField v-model="user.name" />
      <EmailField v-model="user.email" />
      <PhoneField v-model="user.phone" />
      <PasswordField v-model="user.password" />
      <button class="ui button red fluid big" @click="signUpButtonPressed">
    </form>
  </section>
</template>

<script>
...
export default {
  ...
  setup() {
    let user = reactive({
      name: "",
      email: "",
      phone: "",
      password: "",
    });

    const signUpButtonPressed = () => {
      console.log(user);
    };
    return { user, signUpButtonPressed };
  },
};
</script>

I’ve also attached a click event to the signup button and declared a callback function called signUpButtonPressed() inside the setup() function.

As you can see, even though all of the data in the input fields are valid…

The user object properties are empty.

So, What’s happening?

The properties of the user object are bound to the child components that contain the input field but not directly to the input elements.

Bind Input Field Data To The User Object

In order to send the input data to the parent component captured in the v-model directive, we need to add an @input event to the input elements of all child components which are:

  • NameField.vue,
  • EmailField.vue,
  • PhoneField.vue and
  • PasswordField.vue.
@input="$emit('update:modelValue', $event.target.value)"

Once it’s done, the rest is the magic!

Now, I can easily get the errors object using the useFormValidation() function.

import useFormValidation from "@/modules/useFormValidation";
import useSubmitButtonState from "@/modules/useSubmitButtonState";
setup() {
  ...
  const { errors } = useFormValidation();
  const { isSignupButtonDisabled } = useSubmitButtonState(user, errors);
  return { user, isSignupButtonDisabled,}
}

Also, destructure the isSignupButtonDisabled variable by calling the useSubmitButtonState() with the user and errors parameters.

Add it to the returned object.

In the template, all we have to do is to add the :disabled attribute passing the isSignupButtonDisabled variable in quotes.

Use the isSIgnupButtonDisabled property in the template.

<button
    class="ui button red fluid big"
    :disabled="isSignupButtonDisabled"
>
   SIGN UP
</button>

That’s it!

When all of the inputs are valid and the sign-up button is pressed, we can see all of the user data added to the user object in the developer console.

Now we can see the magic!

Reusable Login Page Validation

Now, I can show you how easy it is to implement the Login Page with client-side validation.

And it works like a charm!

Login.vue

<template>
  <section class="signup-view">
    <form @submit.prevent novalidate class="ui form">
      <div class="ui stacked segment">
        <EmailField v-model="user.email" />
        <PasswordField v-model="user.password" />
        <button
          class="ui button red fluid"
          :disabled="isSignupButtonDisabled"
          @click="loginButtonPressed"
        >
          LOG IN
        </button>
      </div>
    </form>
  </section>
</template>

<script>
import { reactive } from "vue";
import EmailField from "@/components/EmailField";
import PasswordField from "@/components/PasswordField";

import useFormValidation from "@/modules/useFormValidation";
import useSubmitButtonState from "@/modules/useSubmitButtonState";

export default {
  components: {
    EmailField,
    PasswordField,
  },
  setup() {
    let user = reactive({
      email: "",
      password: "",
    });

    const { error } = useFormValidation();
    const { isSignupButtonDisabled } = useSubmitButtonState(user, error);

    const loginButtonPressed = () => {
      console.log(user);
    };

    return {
      user,
      isSignupButtonDisabled,
      loginButtonPressed,
    };
  },
};
</script>

Conclusion

In this article, you’ve learned how to create reusable sign-up form client-side validation in Vue 3 Composition API on keyup as well as on blur.

I showed you how to create a reusable child component for each input field with inline and multiple validations using keyup and blur events.

After that, you learned how to change the state of the sign-up button based on the validity of all of the input fields.

Finally, you’ve seen how easy it is to reuse the form validation component to the Login page.

You can find a full source code here

Next
Reusable Module Vs Component In Vue 3 Composition API