JAVASCRIPT

Building a Reusable Form Validation Composable in Vue 3

Create a custom composable in Vue 3 to encapsulate form validation logic, making it reusable across different forms and input fields with ease.

// src/composables/useFormValidation.js
import { ref, reactive, computed } from 'vue';

export function useFormValidation(formFields) {
  const errors = reactive({}); // Stores validation errors
  const touched = reactive({}); // Tracks if a field has been interacted with

  const validateField = (fieldName, value, rules) => {
    errors[fieldName] = [];
    for (const rule of rules) {
      if (!rule.validator(value)) {
        errors[fieldName].push(rule.message);
      }
    }
  };

  const validateAllFields = () => {
    let isValid = true;
    for (const fieldName in formFields) {
      touched[fieldName] = true; // Mark all fields as touched for full validation visibility
      validateField(fieldName, formFields[fieldName].value, formFields[fieldName].rules);
      if (errors[fieldName].length > 0) {
        isValid = false;
      }
    }
    return isValid;
  };

  const getErrorMessage = (fieldName) => {
    return touched[fieldName] && errors[fieldName]?.length > 0 ? errors[fieldName][0] : '';
  };

  const isFormValid = computed(() => {
    return Object.values(errors).every(fieldErrors => fieldErrors.length === 0);
  });

  // Mark field as touched on blur/change to show error only when appropriate
  const onFieldBlur = (fieldName) => {
    touched[fieldName] = true;
  };

  return {
    errors,
    touched,
    validateField,
    validateAllFields,
    getErrorMessage,
    isFormValid,
    onFieldBlur,
  };
}

// src/components/RegistrationForm.vue
<template>
  <form @submit.prevent="submitForm">
    <div>
      <label for="username">Username:</label>
      <input
        type="text"
        id="username"
        v-model="formData.username"
        @blur="onFieldBlur('username')"
        @input="validateField('username', formData.username, formSchema.username.rules)"
      />
      <span class="error">{{ getErrorMessage('username') }}</span>
    </div>
    <div>
      <label for="email">Email:</label>
      <input
        type="email"
        id="email"
        v-model="formData.email"
        @blur="onFieldBlur('email')"
        @input="validateField('email', formData.email, formSchema.email.rules)"
      />
      <span class="error">{{ getErrorMessage('email') }}</span>
    </div>
    <button type="submit" :disabled="!isFormValid">Register</button>
  </form>
</template>

<script setup>
import { reactive } from 'vue';
import { useFormValidation } from '@/composables/useFormValidation'; // Adjust path as needed

const formData = reactive({
  username: '',
  email: ''
});

const formSchema = {
  username: {
    // Value will be dynamically linked below
    rules: [
      { validator: (value) => value.length >= 3, message: 'Username must be at least 3 characters.' },
      { validator: (value) => /^[a-zA-Z0-9]+$/.test(value), message: 'Username can only contain letters and numbers.' },
    ],
  },
  email: {
    // Value will be dynamically linked below
    rules: [
      { validator: (value) => !!value, message: 'Email is required.' },
      { validator: (value) => /.+@.+\..+/.test(value), message: 'Please enter a valid email address.' },
    ],
  },
};

// A more direct way to link schema values to formData for reactive updates:
Object.keys(formSchema).forEach(fieldName => {
    Object.defineProperty(formSchema[fieldName], 'value', {
        get: () => formData[fieldName]
    });
});

// Initialize the composable with the form schema
const { validateField, validateAllFields, getErrorMessage, isFormValid, onFieldBlur } = useFormValidation(formSchema);

const submitForm = () => {
  if (validateAllFields()) {
    alert('Form submitted successfully!
' + JSON.stringify(formData));
    // Proceed with API call or further processing
  } else {
    alert('Please correct the form errors.');
  }
};
</script>

<style scoped>
.error { color: red; font-size: 0.8em; }
div { margin-bottom: 10px; }
</style>
How it works: This snippet provides a flexible `useFormValidation` composable for Vue 3. It manages validation rules, errors, and 'touched' states for form fields. Components integrate it by defining a schema for their `reactive` form data. The composable provides functions to validate individual fields on input/blur and all fields on submission, returning error messages and a computed property `isFormValid` for button enabling.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs