@uplink-protocol/form-controller
Version:
Reactive multi-step form controller with dynamic validation and state management
775 lines (630 loc) • 18.9 kB
Markdown
# Validation Guide
This document provides a comprehensive guide to all validation capabilities in the @uplink-protocol/form-controller package.
## Table of Contents
1. [Validation Overview](#validation-overview)
2. [Built-in Validators](#built-in-validators)
3. [Dynamic Validators](#dynamic-validators)
4. [Custom Validators](#custom-validators)
5. [Validation Context](#validation-context)
6. [Error Messages](#error-messages)
7. [Multiple Error Collection](#multiple-error-collection)
8. [Enhanced Validation Features](#enhanced-validation-features)
9. [Advanced Validation Scenarios](#advanced-validation-scenarios)
## Validation Overview
The form-controller offers a robust validation system with multiple layers of validation:
- **Field validation**: Applied to individual fields
- **Step validation**: Applied to each form step
- **Form validation**: Applied to the entire form
Validation can be:
- **Declarative**: Defined in field configuration
- **Imperative**: Triggered through validation methods
- **Reactive**: Automatically triggered during navigation/submission
### Validation Interface
```typescript
interface Validation {
required?: boolean;
pattern?: RegExp | string;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
custom?: (value: any) => boolean | string;
// Legacy dynamic validator (deprecated)
dynamicValidator?: string;
dynamicValidatorParams?: Record<string, any>;
// Enhanced features
dynamicValidators?: Array<{
name: string;
params?: Record<string, any>;
errorMessage?: string;
}>;
errorMessage?: string;
errorMessages?: {
required?: string;
pattern?: string;
minLength?: string;
maxLength?: string;
min?: string;
max?: string;
email?: string;
custom?: string;
};
collectAllErrors?: boolean;
}
```
### Validation Priority
Validation rules are evaluated in the following order:
1. **Required validation**: Always checked first, regardless of validation order.
2. **Type-specific validations**: Based on field type (email, pattern, length, numeric range, etc.)
3. **Custom validation**: User-defined validation functions.
4. **Dynamic validation**: Context-aware validation using registered validators.
By default, validation stops on the first error encountered. To collect all validation errors, see the [Multiple Error Collection](#multiple-error-collection) section.
## Built-in Validators
### Required Validation
Ensures a field has a value:
```javascript
{
id: 'username',
type: 'text',
label: 'Username',
required: true // Shorthand for validation: { required: true }
}
```
Or explicitly:
```javascript
{
id: 'username',
type: 'text',
label: 'Username',
validation: {
required: true,
errorMessage: 'Username is required'
}
}
```
### Pattern Validation
Validates against a regular expression pattern:
```javascript
{
id: 'email',
type: 'email',
label: 'Email Address',
validation: {
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
errorMessage: 'Please enter a valid email address'
}
}
```
### Length Validation
Validates text length:
```javascript
{
id: 'password',
type: 'password',
label: 'Password',
validation: {
required: true,
minLength: 8,
maxLength: 64,
errorMessage: 'Password must be between 8 and 64 characters'
}
}
```
### Numeric Range Validation
Validates numeric values within a range:
```javascript
{
id: 'age',
type: 'number',
label: 'Age',
validation: {
required: true,
min: 18,
max: 120,
errorMessage: 'Age must be between 18 and 120'
}
}
```
### Custom Inline Validation
Provides a custom validation function:
```javascript
{
id: 'zipCode',
type: 'text',
label: 'Zip Code',
validation: {
custom: (value) => {
if (!value) return true; // Skip if empty
const isValid = /^\d{5}(-\d{4})?$/.test(value);
return isValid ? true : 'Please enter a valid US zip code';
}
}
}
```
## Dynamic Validators
Dynamic validators can access the entire form state and react to other field values.
### Built-in Dynamic Validators
#### requiredIf
Makes a field required based on conditions:
```javascript
// Legacy approach (deprecated)
{
id: 'otherReason',
type: 'text',
label: 'Please specify',
validation: {
dynamicValidator: 'requiredIf',
dynamicValidatorParams: {
condition: 'equals',
fields: ['reason'],
value: 'other',
errorMessage: 'Please specify the reason'
}
}
}
// New approach with dynamicValidators array
{
id: 'otherReason',
type: 'text',
label: 'Please specify',
validation: {
dynamicValidators: [
{
name: 'requiredIf',
params: {
condition: 'equals',
fields: ['reason'],
value: 'other'
},
errorMessage: 'Please specify the reason'
}
]
}
}
```
Supported conditions:
- `equals`: Required when a specified field equals a specific value
- `notEquals`: Required when a specified field doesn't equal a specific value
- `notEmpty`: Required when specified fields have values
#### equals
Validates that a field equals a specific value or another field's value:
```javascript
// Legacy approach (deprecated)
{
id: 'confirmPassword',
type: 'password',
label: 'Confirm Password',
validation: {
dynamicValidator: 'equals',
dynamicValidatorParams: {
targetField: 'password',
errorMessage: 'Passwords do not match'
}
}
}
// New approach with dynamicValidators array
{
id: 'confirmPassword',
type: 'password',
label: 'Confirm Password',
validation: {
dynamicValidators: [
{
name: 'equals',
params: {
targetField: 'password'
},
errorMessage: 'Passwords do not match'
}
]
}
}
```
### Multiple Dynamic Validators
You can apply multiple dynamic validators to a single field:
```javascript
{
id: 'username',
type: 'text',
label: 'Username',
validation: {
required: true,
minLength: 3,
dynamicValidators: [
{
name: 'uniqueUsername',
params: { checkDatabase: true },
errorMessage: 'This username is already taken'
},
{
name: 'noSpecialChars',
errorMessage: 'Username cannot contain special characters'
}
],
collectAllErrors: true
}
}
```
### Custom Dynamic Validators
You can create and register your own dynamic validators:
errorMessage: 'Passwords must match'
}
}
}
```
## Custom Validators
### Registering Custom Validators
You can create and register your own dynamic validators:
```javascript
form.methods.registerValidator('matchesPassword', (value, context) => {
if (!value) return true; // Skip if empty
if (value !== context.formData.password) {
return 'Passwords do not match';
}
return true;
});
```
### Using Custom Validators
Once registered, you can use your custom validators in field configuration:
```javascript
{
id: 'confirmPassword',
type: 'password',
label: 'Confirm Password',
validation: {
dynamicValidator: 'matchesPassword'
}
}
```
### Unregistering Validators
You can remove custom validators when no longer needed:
```javascript
form.methods.unregisterValidator('matchesPassword');
```
## Validation Context
Dynamic validators receive a context object with the following properties:
```typescript
interface ValidatorContext {
field: Field; // The field configuration
formData: Record<string, any>; // The entire form data (flattened)
stepId: string; // The ID of the current step
fieldId: string; // The ID of the field being validated
}
```
This allows validators to:
- Access other field values
- Read field configuration
- Know which step is being validated
- Make context-aware validation decisions
## Error Messages
Error messages can be specified in several ways:
### Default Error Messages
If no custom message is provided, the validator generates a default message.
### Custom Error Messages in Field Configuration
```javascript
{
id: 'email',
type: 'email',
label: 'Email Address',
validation: {
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
errorMessage: 'Please enter a valid email address'
}
}
```
### Per-Validation Error Messages
You can specify different error messages for each type of validation:
```javascript
{
id: 'password',
type: 'password',
label: 'Password',
validation: {
required: true,
minLength: 8,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
errorMessages: {
required: 'Password is required',
minLength: 'Password must be at least 8 characters long',
pattern: 'Password must include uppercase, lowercase, and numbers'
}
}
}
```
### Error Messages from Validator Functions
```javascript
validation: {
custom: (value) => {
if (!value) return true;
return isValid ? true : 'Custom error message';
}
}
```
### Error Messages from Dynamic Validators
For legacy dynamic validators:
```javascript
validation: {
dynamicValidator: 'requiredIf',
dynamicValidatorParams: {
// ...parameters
errorMessage: 'This field is required based on your other selections'
}
}
```
For new multiple dynamic validators:
```javascript
validation: {
dynamicValidators: [
{
name: 'uniqueUsername',
params: { checkExisting: true },
errorMessage: 'This username is already taken'
}
]
}
```
## Multiple Error Collection
By default, validation stops at the first error encountered. However, you can configure fields to collect all validation errors by using the `collectAllErrors` flag:
```javascript
{
id: 'password',
type: 'password',
label: 'Password',
validation: {
required: true,
minLength: 8,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
custom: (value) => {
if (value.includes('password')) {
return 'Password should not contain the word "password"';
}
return true;
},
collectAllErrors: true // Enable collecting all validation errors
}
}
```
### Benefits of Multiple Error Collection
- **Improved UX**: Users can see all validation issues at once
- **Efficient Form Completion**: Reduces the number of submission attempts
- **Comprehensive Feedback**: Especially useful for complex fields
### Accessing Multiple Errors
When validating a field programmatically, you can access all collected errors:
```javascript
// In custom code that uses the validator directly
const result = validateField(field, value, context);
if (!result.isValid) {
console.log('First error:', result.error);
console.log('All errors:', result.errors);
}
```
For more details, see the [Multiple Errors Validation](./multiple-errors-validation.md) documentation.
## Enhanced Validation Features
The form-controller provides enhanced validation capabilities that make it easier to build sophisticated forms with rich validation feedback.
### Per-Validation Error Messages
You can specify different error messages for each validation rule using the `errorMessages` object:
```javascript
{
id: 'password',
type: 'password',
label: 'Password',
validation: {
required: true,
minLength: 8,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
errorMessages: {
required: 'Password is required',
minLength: 'Password must be at least 8 characters long',
pattern: 'Password must include uppercase, lowercase, and numbers'
}
}
}
```
The supported error message keys include:
- `required`: For required field validation
- `pattern`: For pattern/regex validation
- `minLength`: For minimum text length validation
- `maxLength`: For maximum text length validation
- `min`: For minimum numeric value validation
- `max`: For maximum numeric value validation
- `email`: For email format validation
- `custom`: For custom validation functions
### Multiple Dynamic Validators
You can apply multiple dynamic validators to a single field:
```javascript
{
id: 'username',
type: 'text',
label: 'Username',
validation: {
required: true,
dynamicValidators: [
{
name: 'uniqueUsername',
params: { checkDatabase: true },
errorMessage: 'This username is already taken'
},
{
name: 'noSpecialChars',
errorMessage: 'Username cannot contain special characters'
}
]
}
}
```
Each validator in the array can have:
- `name`: The registered validator name
- `params`: Custom parameters for the validator
- `errorMessage`: Specific error message for this validator
### Combining Enhanced Features
You can combine all enhanced validation features for maximum flexibility:
```javascript
{
id: 'password',
type: 'password',
label: 'Password',
validation: {
required: true,
minLength: 8,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
// Per-validation error messages
errorMessages: {
required: 'Password is required',
minLength: 'Password must be at least 8 characters',
pattern: 'Password must include uppercase, lowercase, and numbers'
},
// Multiple dynamic validators with custom parameters
dynamicValidators: [
{
name: 'passwordStrength',
params: { minStrength: 3 },
errorMessage: 'Your password is too weak'
},
{
name: 'notPreviouslyUsed',
params: { historyCheck: true },
errorMessage: 'You cannot reuse a previous password'
}
],
// Collect all validation errors
collectAllErrors: true
}
}
```
See the [Multiple Errors Validation](./multiple-errors-validation.md) document for more examples and best practices.
## Advanced Validation Scenarios
### Asynchronous Validation
You can implement asynchronous validation by combining the form controller with your own async logic:
```javascript
// Field definition
const usernameField = {
id: 'username',
type: 'text',
label: 'Username',
validation: {
required: true
}
};
// Async validation handler
const checkUsernameAvailability = async (username) => {
// Clear any existing async errors
form.methods.clearFieldError('account', 'username');
try {
const response = await fetch(`/api/check-username?username=${username}`);
const { isAvailable } = await response.json();
if (!isAvailable) {
form.methods.setFieldError('account', 'username', 'Username already taken');
return false;
}
return true;
} catch (error) {
console.error('Username validation error:', error);
form.methods.setFieldError('account', 'username', 'Could not validate username');
return false;
}
};
// Connect to UI
usernameInput.addEventListener('blur', async (e) => {
const username = e.target.value;
// First apply standard validation
const isValid = form.methods.validateField('account', 'username', username);
// If basic validation passes, check availability
if (isValid && username) {
const isAvailable = await checkUsernameAvailability(username);
// Update UI based on result
}
});
```
### Cross-Step Validation
You can implement validation that depends on fields in different steps:
```javascript
// Register a validator that checks across steps
form.methods.registerValidator('uniqueInList', (value, context) => {
if (!value) return true;
// Get items from another step
const existingItems = context.formData.itemList || [];
// Skip validation for the current item being edited
const currentItemId = context.formData.currentItemId;
const isDuplicate = existingItems.some(item =>
item.name === value && item.id !== currentItemId
);
return isDuplicate ? 'This name already exists in your list' : true;
});
```
### Conditional Field Requirements
Make fields conditionally required based on complex logic:
```javascript
// Payment method scenario
{
id: 'cardNumber',
type: 'text',
label: 'Card Number',
validation: {
dynamicValidator: 'requiredIf',
dynamicValidatorParams: {
condition: 'equals',
fields: ['paymentMethod'],
value: 'credit-card',
errorMessage: 'Card number is required for credit card payments'
}
}
}
```
### Form-Level Validation
Implement validation at the form level:
```javascript
const submitForm = () => {
// First validate all fields
const isValid = form.methods.validateForm(true);
if (isValid) {
const formData = form.methods.getFlatData();
// Perform form-level validation
if (formData.endDate && formData.startDate) {
const startDate = new Date(formData.startDate);
const endDate = new Date(formData.endDate);
if (endDate < startDate) {
// Set a form-level error
setFormError('End date must be after start date');
return false;
}
}
// Submit the data
submitToServer(formData);
}
};
```
### Complex Validation Rules
For very complex validation rules, you can create a dedicated validator:
```javascript
form.methods.registerValidator('passwordStrength', (value, context) => {
if (!value) return true;
let strength = 0;
let feedback = '';
// Length check
if (value.length >= 8) strength += 1;
// Complexity checks
if (/[A-Z]/.test(value)) strength += 1;
if (/[a-z]/.test(value)) strength += 1;
if (/[0-9]/.test(value)) strength += 1;
if (/[^A-Za-z0-9]/.test(value)) strength += 1;
// Custom password policy
if (value.toLowerCase().includes(context.formData.username?.toLowerCase())) {
feedback = 'Password cannot contain your username';
strength = 0;
}
// Return feedback based on strength
switch (strength) {
case 0:
case 1:
return feedback || 'Password is very weak';
case 2:
return feedback || 'Password is weak';
case 3:
return true; // Acceptable
case 4:
case 5:
return true; // Strong
}
});
```
This validation guide provides a comprehensive overview of all validation capabilities in the form-controller package.