The right way to handle validation in building reusable input components

May 04, 2025 · 4 min read
Share on

Reusable form components are great—until you try to add validation. Then it often becomes a mess of responsibilities: Should the input handle validation itself? Should the parent manage everything, including how to handle its input's validation status? What about context-specific validation rules?

In this article, we’ll walk through the a recommended design pattern for building reusable input components with validation in Lightning Web Components (LWC)—with separation of concerns, extensibility, and customization at its core.

Disclaimer: even though the tech used is LWC, the pattern can easily be applied to other front-end frameworks, like Vue.js, Web Components, React, etc.


The Challenge

Our TextInput works a common component for building a form consisting of input fields for user's text, such as address, phone, email, etc. It wrap around the native HTML input component, with the following functionalities:

  1. Display a dedicated label for the component.
  2. Manage its internal state validation, base on a common set of rules for required, minlength, maxlength, and pattern.
  3. Display its error status on the UI with the proper style, based on the validation result.
  4. Notify its parent on the current value and the validation status.

Below is a mockup design of how TextInput looks:

Additionally, it offer extensibility from the parent level, including:

  1. Bypass the internal validation status display.
  2. Add additional context-specific validation for its input value.

A good use case is a Address component, where there should be a single error message, instead of per input field.

The Design Approaches

The key approaches we follow in this article will be:

  • Separation of Concern: the component handles and manages its own state, including error validation.
  • Extensibility: parent component can extend the component's error validation logic with more specific rules and get report on run-time about its state status.
  • Customization: parent can control the error UI and override the component's error display strategy with its.

Based on these principles, let's start creating our component.

The TextInput Component

Managing and reporting TextInput's state internally and externally

Extending validation logic

Bypassing the component's error UI

Building Address with TextInput component

Resources


Summary

👉 Subscribe to Building with Maya Podcast 👉 Learn about Vue 3 and TypeScript with my new book Learning Vue!

👉 Follow me on X | LinkedIn.

Like this post or find it helpful? Share it 👇🏼 😉

1. ✅ Separation of Concern

The input component should:

  • Handle its own validation state
  • Track touched, dirty, error, etc.
  • Manage and display validation messages (unless overridden)

It should not need to know what form it’s part of or why the data is needed.

2. 🔁 Extensibility

Sometimes built-in validations aren’t enough. You might need:

  • A check for uniqueness
  • Contextual rules (e.g. different formats for internal vs external users)

The input component should allow the parent to pass in additional validation logic—and be able to run all validations and report back.

3. 🎨 Customization

Not every use case wants the input to display errors directly. Sometimes:

  • The parent wants to control the error UI
  • The input is part of a composite component with its own messaging strategy

The input should allow error display to be disabled or overridden, while still tracking and reporting its state.


Component API Design

Here’s a rough sketch of how this input component might be designed.

Public Props (@api)

@api label;
@api required = false;
@api disabled = false;
@api customValidator; // optional callback function
@api hideError = false; // parent can choose to hide error display

Public Methods

@api validate() {
  // runs internal + external validations
  // returns { isValid: boolean, errorMessage: string }
}

@api reset() {
  // resets internal state (value, touched, error)
}

Custom Event

The component dispatches an event like:

this.dispatchEvent(new CustomEvent('validation', {
  detail: {
    isValid,
    errorMessage,
    value: this.value
  },
  bubbles: true,
  composed: true
}));

Validation Flow Example

  1. User blurs the input.
  2. Input runs its own built-in validation (required, pattern, etc.)
  3. If a customValidator is provided, run that too.
  4. If validation fails:
    • Set internal errorMessage
    • Dispatch validation event to parent
    • Optionally display error unless hideError is true

Parent Usage Example

<c-smart-input
  label="Username"
  required
  custom-validator={validateUsername}
  onvalidation={handleValidation}
  hide-error
></c-smart-input>

<p if:true={error}>{error}</p>
handleValidation(event) {
  const { isValid, errorMessage } = event.detail;
  this.error = isValid ? '' : `Oops: ${errorMessage}`;
}

validateUsername(value) {
  return value.startsWith('user-') ? true : 'Username must start with user-';
}
Share on

Learning Vue

Learn the core concepts of Vue.js, the modern JavaScript framework for building frontend applications and interfaces from scratch

Get a copy
Learning Vue