Overview
Validation rules are functions that validate form field values and return error messages when validation fails. These rules are used by the form engine to provide real-time field validation.
Import
import { required } from '@shared/validation-engine/rules'
Validator Function Type
All validation functions follow this signature:
type ValidatorFnType = (value: string, fieldName: string) => string | null
Parameters:
value: The current field value to validate
fieldName: The name/ID of the field being validated (used in error messages)
Returns:
string: Error message if validation fails
null: If validation passes
Available Validators
required()
Validates that a field has a non-empty value.
function required(value: string, fieldName: string): string | null
The field value to validate
The field identifier, used in the error message
Returns:
null if the value (after trimming whitespace) is not empty
"{fieldName} is required" if the value is empty or contains only whitespace
Example:
import { required } from '@shared/validation-engine/rules'
const error1 = required('John', 'name')
// Returns: null (valid)
const error2 = required('', 'email')
// Returns: "email is required"
const error3 = required(' ', 'password')
// Returns: "password is required" (whitespace-only is invalid)
Validators are typically defined in the form schema configuration, not called directly:
import useFormEngine from '@app/hooks/useFormEngine'
const formSchema = [
{
id: 'email',
label: 'Email Address',
type: 'email',
placeholder: 'Enter your email',
fieldValidators: [
{ type: 'required' }, // Maps to the required() function
{ type: 'minLength', constraints: { minLength: 5 } }
]
},
{
id: 'username',
label: 'Username',
type: 'text',
placeholder: 'Choose a username',
fieldValidators: [
{ type: 'required' }
]
}
]
const { formData, onChange, errors } = useFormEngine(formSchema)
How Validation Works
-
Schema Definition: You define validators in the form schema using validator type objects:
-
Compilation: The
useFormEngine hook compiles these into actual validator functions using attachValidators
-
Execution: When a field changes, validators run sequentially:
for (let validator of fieldSchema.fieldValidators) {
const error = validator(value, fieldId)
if (error) {
// Stop at first error
break
}
}
-
Error Display: The first validation error (if any) is returned and can be displayed to the user
Validator Types Configuration
Validators are configured using these type definitions:
type FieldValidatorType =
| { type: "required" }
| { type: "minLength", constraints: { minLength: number } }
| { type: "maxLength", constraints: { minLength: number } }
Required Validator
No additional configuration needed.
MinLength Validator
{
type: 'minLength',
constraints: {
minLength: 8 // Minimum number of characters
}
}
MaxLength Validator
{
type: 'maxLength',
constraints: {
minLength: 100 // Maximum number of characters
}
}
Creating Custom Validators
To create a custom validator, follow the ValidatorFnType signature:
function email(value: string, fieldName: string): string | null {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) {
return `${fieldName} must be a valid email address`
}
return null
}
Factory Pattern for Configurable Validators
type ValidatorFactory = (constraints?: any) => ValidatorFnType
function minLength(constraints: { minLength: number }): ValidatorFnType {
return (value: string, fieldName: string) => {
if (value.length < constraints.minLength) {
return `${fieldName} must be at least ${constraints.minLength} characters`
}
return null
}
}
// Usage
const validator = minLength({ minLength: 8 })
const error = validator('short', 'password')
// Returns: "password must be at least 8 characters"
Validation Flow Example
Here’s how validation works in a complete form:
import useFormEngine from '@app/hooks/useFormEngine'
function RegistrationForm() {
const schema = [
{
id: 'email',
fieldValidators: [{ type: 'required' }]
},
{
id: 'password',
fieldValidators: [
{ type: 'required' },
{ type: 'minLength', constraints: { minLength: 8 } }
]
}
]
const { formData, onChange, errors } = useFormEngine(schema)
return (
<form>
<input
type="email"
value={formData.email || ''}
onChange={(e) => onChange(e, 'email')}
/>
{/* Shows: "email is required" if empty */}
{errors.email && <span>{errors.email}</span>}
<input
type="password"
value={formData.password || ''}
onChange={(e) => onChange(e, 'password')}
/>
{/* Shows first error: "password is required" OR
"password must be at least 8 characters" */}
{errors.password && <span>{errors.password}</span>}
</form>
)
}
Best Practices
-
Order Matters: Place validators in order of priority. The first failing validator determines the error message:
fieldValidators: [
{ type: 'required' }, // Check required first
{ type: 'minLength', ... } // Then check length
]
-
Clear Error Messages: Use descriptive field names that make sense in error messages:
{ id: 'email', ... } // Good: "email is required"
{ id: 'field1', ... } // Bad: "field1 is required"
-
Whitespace Handling: The
required validator trims whitespace, preventing users from bypassing validation with spaces
-
Combine Multiple Validators: Use multiple validators for comprehensive validation:
fieldValidators: [
{ type: 'required' },
{ type: 'minLength', constraints: { minLength: 8 } },
{ type: 'maxLength', constraints: { minLength: 50 } }
]
Source
Implemented in: src/shared/validation-engine/rules.ts:1