Combining & Registering Validation
Assembling all validators and integrating with the form.
Overviewβ
We've created validators for each step plus cross-step validation. Now let's:
- Create the main validation file that combines everything
- Register validation with the form
- Test that all validation works together
- Review the complete file structure
Creating the Main Validation Fileβ
Create the main validation file that imports and applies all step validators:
touch src/schemas/validators/credit-application.ts
Implementationβ
import type { ValidationSchemaFn, FieldPath } from '@reformer/core';
import type { CreditApplicationForm } from '@/types';
// Import step validators
import { loanValidation } from './loan-info';
import { personalValidation } from './personal-info';
import { contactValidation } from './contact-info';
import { employmentValidation } from './employment';
import { additionalValidation } from './additional-info';
import { crossStepValidation } from './cross-step';
/**
* Complete validation schema for Credit Application Form
*
* Organized by form steps for maintainability:
* - Step 1: Loan Information
* - Step 2: Personal Information
* - Step 3: Contact Information
* - Step 4: Employment
* - Step 5: Additional Information
* - Cross-Step: Validation spanning multiple steps
*/
export const creditApplicationValidation: ValidationSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
// ==========================================
// Step 1: Loan Information
// ==========================================
loanValidation(path);
// ==========================================
// Step 2: Personal Information
// ==========================================
personalValidation(path);
// ==========================================
// Step 3: Contact Information
// ==========================================
contactValidation(path);
// ==========================================
// Step 4: Employment
// ==========================================
employmentValidation(path);
// ==========================================
// Step 5: Additional Information
// ==========================================
additionalValidation(path);
// ==========================================
// Cross-Step Validation
// ==========================================
crossStepValidation(path);
};
Registering with the Formβ
Update your form creation function to include validation:
import { createForm } from '@reformer/core';
import { creditApplicationSchema } from './credit-application.schema';
import { creditApplicationBehaviors } from '../behaviors/credit-application.behaviors';
import { creditApplicationValidation } from '../validators/credit-application.validators';
import type { CreditApplicationForm } from '@/types';
export function createCreditApplicationForm() {
return createForm<CreditApplicationForm>({
schema: creditApplicationSchema,
behaviors: creditApplicationBehaviors,
validation: creditApplicationValidation, // β Register validation here
});
}
That's it! Validation is now active when the form is created.
File Structureβ
Your project should now have this structure:
src/
βββ schemas/
β βββ validators/
β β βββ loan-info.ts
β β βββ personal-info.ts
β β βββ contact-info.ts
β β βββ employment.ts
β β βββ additional-info.ts
β β βββ cross-step.validators.ts
β β βββ credit-application.validators.ts β Main file
β βββ behaviors/
β β βββ loan-info.ts
β β βββ personal-info.ts
β β βββ contact-info.ts
β β βββ employment.ts
β β βββ additional-info.ts
β β βββ cross-step.behaviors.ts
β β βββ credit-application.behaviors.ts
β βββ credit-application.ts
β βββ create-form.ts β Validation registered here
β
βββ components/
β βββ forms/
β β βββ createCreditApplicationForm.ts
β βββ steps/
β βββ nested-forms/
β βββ CreditApplicationForm.tsx
β
βββ types/
βββ credit-application.ts
Testing All Validationβ
Create a comprehensive test checklist:
Step 1: Loan Informationβ
- Required fields (loanType, loanAmount, loanTerm, loanPurpose)
- Numeric ranges (amount 50k-10M, term 6-360 months)
- String length (purpose 10-500 chars)
- Conditional mortgage fields (propertyValue, initialPayment)
- Conditional car loan fields (brand, model, year, price)
Step 2: Personal Informationβ
- Required names with Cyrillic pattern
- Birth date not in future
- Age 18-70 validation
- Passport series (4 digits) and number (6 digits)
- Passport issue date validation
- INN (10 or 12 digits) and SNILS (11 digits)
Step 3: Contact Informationβ
- Required main phone and email
- Optional additional phone and email (validated if provided)
- Required registration address fields
- Conditional residence address (when sameAsRegistration = false)
- Postal code format (6 digits)
Step 4: Employmentβ
- Required employment status
- Required monthly income (min 10,000)
- Conditional company fields (when employed)
- Work experience >= 3 months (when employed)
- Conditional business fields (when self-employed)
- Business experience >= 6 months (when self-employed)
Step 5: Additional Informationβ
- Properties array (min 1 when hasProperty, max 10)
- Property element validation (type, description, value)
- Existing loans array (min 1 when hasExistingLoans, max 20)
- Loan element validation (bank, amount, payment)
- Co-borrowers array (min 1 when hasCoBorrower, max 5)
- Co-borrower element validation (name, email, phone, income)
Cross-Stepβ
- Down payment >= 20% of property value
- Monthly payment <= 50% of household income
- Loan amount <= car price
- Remaining loan <= original loan amount
- Age validation on computed field
- Async INN verification (shows loading, validates)
- Async SNILS verification
- Async email uniqueness check
Debugging Validationβ
If validation doesn't work as expected:
1. Check Console for Errorsβ
// Add debug logging to validators
export const loanValidation: ValidationSchemaFn<CreditApplicationForm> = (path) => {
console.log('Registering Step 1 validation');
required(path.loanAmount, { message: 'Loan amount is required' });
console.log('Added required validator for loanAmount');
};
2. Verify Field Pathsβ
Incorrect field paths cause validation to silently fail:
// β Wrong - typo in field name
required(path.loanAmmount, { message: '...' });
// β
Correct
required(path.loanAmount, { message: '...' });
3. Check Form Registrationβ
Ensure validation is passed to createForm:
// β Forgot to add validation
createForm({
schema: creditApplicationSchema,
behaviors: creditApplicationBehaviors,
});
// β
Validation registered
createForm({
schema: creditApplicationSchema,
behaviors: creditApplicationBehaviors,
validation: creditApplicationValidation,
});
4. Verify Component Integrationβ
Make sure you're using the form with validation:
function CreditApplicationForm() {
const form = useMemo(() => createCreditApplicationForm(), []); // β Uses validation
return <FormField control={form.loanAmount} />;
}
5. Check Field Statusβ
Debug field validation state:
function DebugField({ control }: { control: FieldNode<any> }) {
const errors = control.errors.value;
const isValid = control.isValid.value;
const isValidating = control.isValidating.value;
console.log('Field errors:', errors);
console.log('Is valid:', isValid);
console.log('Is validating:', isValidating);
return <FormField control={control} />;
}
Validation Execution Orderβ
Understanding when validation runs:
1. On Field Changeβ
form.field('loanAmount').setValue(100000);
// β Triggers all validators for loanAmount
// β Triggers validators that depend on loanAmount
2. On Dependency Changeβ
form.field('loanType').setValue('mortgage');
// β Re-runs conditional validators:
// - requiredWhen for propertyValue
// - requiredWhen for initialPayment
// - Cross-step down payment validator
3. On Form Submitβ
// Mark all fields as touched
form.touchAll();
// Get current form values
const data = form.getValue();
// Check if form is valid before sending
if (form.valid.value) {
console.log('Valid data:', data);
} else {
console.log('Validation errors - check form.errors.value');
}
4. Manual Validationβ
// Validate single field
await form.field('loanAmount').validate();
// Validate entire form
await form.validate();
// Validate specific step
await form.group('step1').validate();
Performance Considerationsβ
Validation is optimized by ReFormer, but keep these in mind:
1. Avoid Expensive Sync Validatorsβ
// β Bad - expensive operation on every change
createValidator(path.field, [], (value) => {
return expensiveCalculation(value); // Runs on every keystroke!
});
// β
Better - keep sync validators fast
createValidator(path.field, [], (value) => {
return quickCheck(value);
});
2. Use Debouncing for Async Validatorsβ
// β Bad - API call on every keystroke
createAsyncValidator(path.inn, async (inn) => {
return await fetch(`/api/validate/inn?value=${inn}`);
});
// β
Good - debounced API calls
createAsyncValidator(
path.inn,
async (inn) => {
return await fetch(`/api/validate/inn?value=${inn}`);
},
{ debounce: 500 } // β Debounce
);
3. Minimize Dependenciesβ
// β Bad - unnecessary dependencies
createValidator(
path.field,
[path.a, path.b, path.c, path.d, path.e], // Too many!
(value, deps) => {
/* ... */
}
);
// β
Good - only necessary dependencies
createValidator(
path.field,
[path.dependency], // Only what's needed
(value, [dep]) => {
/* ... */
}
);
4. Don't Create Circular Dependenciesβ
// β Bad - circular dependency
createValidator(path.a, [path.b], (a, [b]) => {
/* ... */
});
createValidator(path.b, [path.a], (b, [a]) => {
/* ... */
}); // Infinite loop!
// β
Good - one-way dependencies
createValidator(path.a, [], (a) => {
/* ... */
});
createValidator(path.b, [path.a], (b, [a]) => {
/* ... */
});
Accessing Validation Stateβ
In Componentsβ
import { useField } from '@reformer/core/react';
function FormField({ control }) {
const field = useField(control);
return (
<div>
<input value={field.value ?? ''} onChange={(e) => control.setValue(e.target.value)} />
{/* Show errors */}
{field.errors.length > 0 && (
<div className="error">
{field.errors.map((error) => (
<div key={error.type}>{error.message}</div>
))}
</div>
)}
{/* Show loading for async validation */}
{field.isValidating && <span>Validating...</span>}
</div>
);
}
In Form Logicβ
const form = createCreditApplicationForm();
// Check if form is valid
const isValid = form.isValid.value;
// Get all errors
const errors = form.errors.value;
// Check specific field
const loanAmountErrors = form.field('loanAmount').errors.value;
// Subscribe to validation changes
form.isValid.subscribe((valid) => {
console.log('Form valid:', valid);
});
Summaryβ
We've successfully implemented complete validation for the Credit Application form:
Step 1: Loan Informationβ
- β Required fields and numeric ranges
- β String length validation
- β Conditional mortgage/car loan fields
Step 2: Personal Informationβ
- β Name validation with Cyrillic pattern
- β Birth date and age validation
- β Passport format validation
- β INN and SNILS patterns
Step 3: Contact Informationβ
- β Email and phone format validation
- β Required address fields
- β Conditional residence address
Step 4: Employmentβ
- β Required income and status
- β Conditional employment fields
- β Conditional self-employment fields
- β Work/business experience minimums
Step 5: Additional Informationβ
- β Array length validation
- β Array element validation
- β Nested object validation in arrays
Cross-Stepβ
- β Down payment >= 20% validation
- β Monthly payment <= 50% income
- β Loan amount <= car price
- β Remaining loan <= original amount
- β Age validation
- β Async INN verification
- β Async SNILS verification
- β Async email uniqueness
Key Achievementsβ
- Declarative Validation - Clear, maintainable validation rules
- Organized Structure - Easy to find and modify validators
- Type Safety - Full TypeScript support
- Conditional Logic - Dynamic validation based on form state
- Cross-Field - Complex business rules across multiple fields
- Async Support - Server-side validation with debouncing
- Works with Behaviors - Perfect synchronization
Validation vs Behaviorsβ
Our form now has both:
| Feature | Behaviors | Validation |
|---|---|---|
| Purpose | Automate interactions | Ensure data quality |
| When runs | On field changes | On field changes + submit |
| Examples | - Show/hide fields - Compute values - Copy data | - Required fields - Format checks - Business rules |
| User feedback | Visual changes | Error messages |
They work together:
- Behaviors hide fields β Validation skips them
- Behaviors compute values β Validation checks them
- Behaviors enable/disable β Validation respects state
What's Next?β
The form now has sophisticated validation, but we still need to handle data flow and submission. In the next sections, we'll cover:
Data Flow (Next Section)β
- Loading initial form data
- Saving form progress (auto-save)
- Resetting form state
- Cloning and duplicating forms
Submission (Following Section)β
- Handling form submission
- Server communication
- Success and error handling
- Optimistic updates
- Retry logic
The validation we've created will seamlessly integrate with these features!