← Back to list

Design A Form Validation Library

Basic Requirements:

amandeep kumar · 2024-02-19 07:50 · 0 claps · 5.2 min read
#react #material-ui #validation #javascript #system-design-interview
Open on Medium ↗
Wiki topics: 🌐 · Web Development 📚 · Books & Reading

Design A Form Validation Library

Basic Requirements:

Supported Validation Rules:

  • Required Field
  • Minimum/Maximum Length
  • Regular Expression Pattern Matching
  • Email Format
  • Date Format
  • Number Range
  • Custom Validation Functions

Error Handling:

  • Clear and user-friendly error messages (inline or separate)
  • Customization of error display styles (inline elements, popovers, tooltip)
  • Ability to suppress validation on specific fields or when the form is disabled

User Experience:

  • Real-time feedback as user types (optional highlighting or subtle hints)
  • Debouncing or throttling for performance and UX optimization
  • Accessibility considerations (screen readers, keyboard navigation)

Flexibility:

  • Support for various input types (text, email, password, number, select, textarea)
  • Dynamic rule configuration during form initialization or runtime
  • Seamless integration with different frontend frameworks (if applicable)

Additional Requirements:

Advanced Validation Rules:

  • Asynchronous validation (e.g., checking username uniqueness)
  • Conditional validation (rules depend on other field values)
  • Cross-field validation (checking the consistency between values)

Form Submission Control:

  • Prevent submission if validation fails (disable the submit button or show an error message)
  • Optional data handling before submission (sanitization, transformations)

Internationalization:

  • Customizable error messages and formatting based on locale
  • Support for right-to-left languages

The architecture of Components:

  1. Validator:
  • Responsible for:
  • Accepting data and rules
  • Performing validation checks
  • Generating error messages
  • Providing additional data (e.g., warnings)

Should be:

  • Modular and reusable for different field types
  • Able to handle both basic and advanced rules
  • Extendable for custom validation logic

2. Rule Manager:

  • Responsible for:
  • Storing and managing validation rules
  • Assigning rules to fields
  • Providing rule configuration API

Should be:

  • Dynamic and flexible
  • Efficient for large forms or many rules
  • Able to handle conditional and cross-field rules

3. Error Handler:

  • Responsible for:
  • Displaying error messages to the user
  • Managing error states (e.g., highlighting fields)
  • Customizing error presentation

Should be:

  • User-friendly and clear
  • Customizable in terms of styling and placement
  • Accessible and keyboard-compatible

4. Form Manager:

  • Responsible for:
  • Coordinating validation across the entire form
  • Handling form submission behavior based on validation results
  • Handling disabled state and dynamic rule updates

Should be:

  • Well-organized and maintainable
  • Efficient for complex forms
  • Flexible for different use cases

Data Models:

  • Field:
  • Contains metadata like field name, type, label, placeholder, initial value
  • May have a reference to associated Validator(s)
  • Rule:
  • Defines the validation criteria (type, parameters, error message)
  • Error:
  • Stores error message, severity level (optional), related field(s)
  • Form:
  • Includes a collection of Fields, rules attached to those fields, and a reference to the Error Handler

Optimization:

  • Debouncing/Throttling: Apply debouncing or throttling to validation checks as users type to improve performance and user experience.
  • Lazy Validation: Don’t perform expensive validation immediately, but delay it until necessary (e.g., on blur or form submission).
  • Memoization: Cache validation results for fields with unchanged values to avoid redundant computations.
  • Efficient Rule Management: Use efficient data structures and algorithms for rule lookups and updates.
  • Lightweight Error Display: Display errors efficiently, considering potential UI impact.

Additional Considerations:

  • Documentation: Provide clear and comprehensive documentation for developers using the library.
  • Testing: Write unit and integration tests to ensure the library’s correctness and robustness.
  • Maintainability: Use a clean and modular code structure for easy maintenance and future development.
  • Community: Consider open-sourcing the library to benefit from community contributions and support.

Version 1

// Validator class
class Validator {
  constructor(field, rules) {
    this.field = field;
    this.rules = rules;
    this.errors = [];
  }

  validate() {
    this.errors = []; // Clear previous errors

    for (const rule of this.rules) {
      const isValid = rule.check(this.field.value);
      if (!isValid) {
        this.errors.push(rule.errorMessage);
      }
    }

    return this.errors.length === 0;
  }

  get isValid() {
    return this.validate();
  }
}

// Rule class
class Rule {
  constructor(type, params, errorMessage) {
    this.type = type;
    this.params = params;
    this.errorMessage = errorMessage;
  }

  check(value) {
    switch (this.type) {
      case "required":
        return value.trim() !== "";
      case "minLength":
        return value.length >= this.params.minLength;
      case "maxLength":
        return value.length <= this.params.maxLength;
      case "pattern":
        return new RegExp(this.params.pattern).test(value);
      case "email":
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
      case "number":
        return !isNaN(value);
      case "range":
        return value >= this.params.min && value <= this.params.max;
      // Add more rule types as needed
      default:
        throw new Error(`Invalid rule type: ${this.type}`);
    }
  }
}

// ErrorHandler class
class ErrorHandler {
  constructor(form) {
    this.form = form;
    this.errorEl = document.createElement("div"); // Container for displaying errors
    this.errorEl.classList.add("form-errors");
    this.form.appendChild(this.errorEl);
  }

  displayErrors(errors) {
    this.errorEl.textContent = ""; // Clear previous errors

    if (errors.length > 0) {
      const ul = document.createElement("ul");
      for (const error of errors) {
        const li = document.createElement("li");
        li.textContent = error;
        ul.appendChild(li);
      }
      this.errorEl.appendChild(ul);
      this.form.classList.add("has-errors"); // Add a CSS class for styling
    } else {
      this.form.classList.remove("has-errors");
    }
  }
}

// FormManager class 
class FormManager {
  constructor(form) {
    this.form = form;
    this.validators = {}; // Map field names to validators
    this.errorHandler = new ErrorHandler(form);

    this.initValidators();
    this.bindEvents();
  }

  initValidators() {
    // Get validation rules from data attributes or another mechanism
    for (const field of this.form.elements) {
      const rules = field.dataset.validation;
      if (rules) {
        const parsedRules = rules.split(",").map(rule => {
          const [type, ...params] = rule.trim().split(":");
          return new Rule(type, params.length > 0 ? JSON.parse(params[0]) : {}, field.dataset.errorMessage || "");
        });
        this.validators[field.name] = new Validator(field, parsedRules);
      }
    }
  }

  bindEvents() {
    this.form.addEventListener("submit", (event) => {
      event.preventDefault(); // Prevent default form submission

      for (const fieldName in this.validators) {
        const validator = this.validators[fieldName];
        if (!validator.isValid) {
          const errors = validator.errors;
          this.errorHandler.displayErrors(errors);
          return; // Stop further processing if there are errors
        }
      }

      // Submit the form if all validations pass
      // TODO: Add logic to handle form submission (e.g., send data to server)
      console.log("Form submitted successfully!");
    });

    // Add event listeners for individual fields as needed
    // (e.g., real-time validation, debouncing)
  }
}

Version 2

import React, { useState, useRef } from 'react';
import TextField from '@mui/material/TextField';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';

// Validator class (similar to previous implementation)
class Validator {
  constructor(field, rules) {
    this.field = field;
    this.rules = rules;
    this.errors = [];
  }

  validate() {
    this.errors = []; // Clear previous errors

    for (const rule of this.rules) {
      const isValid = rule.check(this.field.value);
      if (!isValid) {
        this.errors.push(rule.errorMessage);
      }
    }

    return this.errors.length === 0;
  }

  get isValid() {
    return this.validate();
  }
}

// Rule class (similar to previous implementation)
class Rule {
  constructor(type, params, errorMessage) {
    this.type = type;
    this.params = params;
    this.errorMessage = errorMessage;
  }

  check(value) {
    // ... (same logic as before)
  }
}

const FormManager = () => {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    age: '',
  });
  const [errors, setErrors] = useState({});

  const handleChange = (event) => {
    setFormData({ ...formData, [event.target.name]: event.target.value });
    const validator = validators[event.target.name];
    if (validator) {
      const fieldErrors = validator.errors;
      setErrors({ ...errors, [event.target.name]: fieldErrors });
    }
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    const allErrors = {};
    for (const fieldName in validators) {
      const validator = validators[fieldName];
      if (!validator.isValid) {
        allErrors[fieldName] = validator.errors;
      }
    }

    if (Object.keys(allErrors).length === 0) {
      // Submit the form
      console.log('Form submitted successfully:', formData);
    } else {
      setErrors(allErrors);
    }
  };

  const validators = {
    name: new Validator(formData.name, [
      { type: 'required', errorMessage: 'Name is required' },
      { type: 'minLength', params: { minLength: 3 }, errorMessage: 'Name must be at least 3 characters long' },
    ]),
    email: new Validator(formData.email, [
      { type: 'required', errorMessage: 'Email is required' },
      { type: 'email', errorMessage: 'Invalid email format' },
    ]),
    age: new Validator(formData.age, [
      { type: 'required', errorMessage: 'Age is required' },
      { type: 'number', errorMessage: 'Age must be a number' },
      { type: 'range', params: { min: 18, max: 120 }, errorMessage: 'Age must be between 18 and 120' },
    ]),
  };

  return (
    <form onSubmit={handleSubmit}>
      <TextField
        label="Name"
        name="name"
        value={formData.name}
        onChange={handleChange}
        error={errors.name?.length > 0}
        helperText={errors.name?.join(', ')}
        // Additional MUI error props as needed
      />
      <TextField
        label="Email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        error={errors.email?.length > 0}
        helperText={errors.email?.join(', ')}
        // Additional MUI error props as needed
      />
      <TextField
        label="Age"
        name="age"
        value={formData.age}
        onChange={handleChange}
        error={errors.age?.length > 0}
        helperText={errors.age?.join(', ')}
        // Additional MUI error props as needed
      />
      <button type="submit">Submit</button>
    </form>
  );
};

export default FormManager;

By carefully considering these requirements, components, data models, optimization techniques, and additional considerations, we can create a robust, extensible, user-friendly form validation library for client-side development.


메타데이터
post_id
3cf9d2b6bb85
slug
design-a-form-validation-library-3cf9d2b6bb85
url
https://medium.com/@amanrags/design-a-form-validation-library-3cf9d2b6bb85
canonical_url
https://medium.com/@amanrags/design-a-form-validation-library-3cf9d2b6bb85
author_url
https://medium.com/@amanrags
status
ok
fetched_at
2026-07-24 09:50:29