The right way to handle validation in building reusable input components
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:
- Display a dedicated label for the component.
- Manage its internal state validation, base on a common set of rules for
required
,minlength
,maxlength
, andpattern
. - Display its error status on the UI with the proper style, based on the validation result.
- 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:
- Bypass the internal validation status display.
- 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!
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
- User blurs the input.
- Input runs its own built-in validation (
required
,pattern
, etc.) - If a
customValidator
is provided, run that too. - If validation fails:
- Set internal
errorMessage
- Dispatch
validation
event to parent - Optionally display error unless
hideError
is true
- Set internal
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-';
}
Learning Vue
Learn the core concepts of Vue.js, the modern JavaScript framework for building frontend applications and interfaces from scratch