Skip to main content

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:

  1. Create the main validation file that combines everything
  2. Register validation with the form
  3. Test that all validation works together
  4. 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​

src/schemas/validators/credit-application.ts
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:

src/schemas/create-form.ts
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​

  1. Declarative Validation - Clear, maintainable validation rules
  2. Organized Structure - Easy to find and modify validators
  3. Type Safety - Full TypeScript support
  4. Conditional Logic - Dynamic validation based on form state
  5. Cross-Field - Complex business rules across multiple fields
  6. Async Support - Server-side validation with debouncing
  7. Works with Behaviors - Perfect synchronization

Validation vs Behaviors​

Our form now has both:

FeatureBehaviorsValidation
PurposeAutomate interactionsEnsure data quality
When runsOn field changesOn field changes + submit
Examples- Show/hide fields
- Compute values
- Copy data
- Required fields
- Format checks
- Business rules
User feedbackVisual changesError 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!