Skip to main content

Step 3: Contact Information Validation

Validating email, phone, and address fields with format validators and conditional rules.

What We're Validating​

Step 3 contains contact and address fields:

FieldValidation Rules
phoneMainRequired, phone format
phoneAdditionalOptional, phone format
emailRequired, email format
emailAdditionalOptional, email format
registrationAddress.cityRequired
registrationAddress.streetRequired
registrationAddress.houseRequired
registrationAddress.postalCodeOptional, 6 digits
residenceAddress.*Required when sameAsRegistration is false

Creating the Validator File​

Create the validator file for Step 3:

touch src/schemas/validators/contact-info.ts

Implementation​

Phone and Email Validation​

Start with phone and email format validation:

src/schemas/validators/contact-info.ts
import { required, email, phone, pattern, applyWhen } from '@reformer/core/validators';
import type { ValidationSchemaFn, FieldPath } from '@reformer/core';
import type { CreditApplicationForm } from '@/types';

/**
* Validation for Step 3: Contact Information
*
* Validates:
* - Phone numbers (main and additional)
* - Email addresses (main and additional)
* - Registration address (always required)
* - Residence address (conditionally required)
*/
export const contactValidation: ValidationSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
// ==========================================
// Phone Numbers
// ==========================================

// Main phone (required)
required(path.phoneMain, { message: 'Main phone number is required' });
phone(path.phoneMain, { message: 'Invalid phone format' });

// Additional phone (optional, but must be valid if provided)
phone(path.phoneAdditional, { message: 'Invalid phone format' });

// ==========================================
// Email Addresses
// ==========================================

// Main email (required)
required(path.email, { message: 'Email is required' });
email(path.email, { message: 'Invalid email format' });

// Additional email (optional, but must be valid if provided)
email(path.emailAdditional, { message: 'Invalid email format' });
};
Format Validators

Format validators like email() and phone() automatically skip empty values. That's why we use required() separately for mandatory fields.

Registration Address Validation​

Add validation for registration address (always required):

src/schemas/validators/contact-info.ts
export const contactValidation: ValidationSchemaFn<CreditApplicationForm> = (path) => {
// ... previous validation ...

// ==========================================
// Registration Address (Always Required)
// ==========================================

required(path.registrationAddress.city, { message: 'City is required' });

required(path.registrationAddress.street, { message: 'Street is required' });

required(path.registrationAddress.house, { message: 'House number is required' });

// Apartment is optional, no validation needed

// Postal code (optional, but must be 6 digits if provided)
pattern(path.registrationAddress.postalCode, /^\d{6}$/, {
message: 'Postal code must be 6 digits',
});
};

Residence Address Conditional Validation​

Add conditional validation for residence address (required only when different from registration):

src/schemas/validators/contact-info.ts
export const contactValidation: ValidationSchemaFn<CreditApplicationForm> = (path) => {
// ... previous validation ...

// ==========================================
// Residence Address (Conditionally Required)
// ==========================================

applyWhen(
path.sameAsRegistration,
(same) => !same,
(p) => {
required(p.residenceAddress.city, { message: 'City is required' });
required(p.residenceAddress.street, { message: 'Street is required' });
required(p.residenceAddress.house, { message: 'House number is required' });
}
);

pattern(path.residenceAddress.postalCode, /^\d{6}$/, {
message: 'Postal code must be 6 digits',
});
};

Complete Code​

Here's the complete validator for Step 3:

src/schemas/validators/contact-info.ts
import { required, email, phone, pattern, applyWhen } from '@reformer/core/validators';
import type { ValidationSchemaFn, FieldPath } from '@reformer/core';
import type { CreditApplicationForm } from '@/types';

/**
* Validation for Step 3: Contact Information
*
* Validates:
* - Phone numbers (main and additional)
* - Email addresses (main and additional)
* - Registration address (always required)
* - Residence address (conditionally required)
*/
export const contactValidation: ValidationSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
// ==========================================
// Phone Numbers
// ==========================================

required(path.phoneMain, { message: 'Main phone number is required' });
phone(path.phoneMain, { message: 'Invalid phone format' });

phone(path.phoneAdditional, { message: 'Invalid phone format' });

// ==========================================
// Email Addresses
// ==========================================

required(path.email, { message: 'Email is required' });
email(path.email, { message: 'Invalid email format' });

email(path.emailAdditional, { message: 'Invalid email format' });

// ==========================================
// Registration Address (Always Required)
// ==========================================

required(path.registrationAddress.city, { message: 'City is required' });
required(path.registrationAddress.street, { message: 'Street is required' });
required(path.registrationAddress.house, { message: 'House number is required' });

pattern(path.registrationAddress.postalCode, /^\d{6}$/, {
message: 'Postal code must be 6 digits',
});

// ==========================================
// Residence Address (Conditionally Required)
// ==========================================

applyWhen(
path.sameAsRegistration,
(same) => !same,
(p) => {
required(p.residenceAddress.city, { message: 'City is required' });
required(p.residenceAddress.street, { message: 'Street is required' });
required(p.residenceAddress.house, { message: 'House number is required' });
}
);

pattern(path.residenceAddress.postalCode, /^\d{6}$/, {
message: 'Postal code must be 6 digits',
});
};

How It Works​

Email Validator​

email(path.email, { message: 'Invalid email format' });
  • Built-in email format validation
  • Checks for basic email structure: user@domain.com
  • Skips empty values (doesn't trigger on empty fields)
  • Use with required() for mandatory fields

Phone Validator​

phone(path.phoneMain, { message: 'Invalid phone format' });
  • Built-in phone format validation
  • Supports various formats:
    • +7 (999) 123-45-67
    • +79991234567
    • 89991234567
    • 9991234567
  • Skips empty values
  • Use with required() for mandatory fields

Conditional Required​

applyWhen(
path.sameAsRegistration, // ← Watch this field
(same) => !same, // ← Condition: apply when false
(p) => {
required(p.residenceAddress.city, { message: 'City is required' });
required(p.residenceAddress.street, { message: 'Street is required' });
required(p.residenceAddress.house, { message: 'House number is required' });
}
);

How it works:

  1. Watches sameAsRegistration field
  2. When sameAsRegistration changes, re-evaluates condition
  3. If condition returns true, applies validators in callback
  4. If condition returns false, validators are not applied

Integration with Behaviors​

Remember from Behaviors section:

// Behavior: Hide residence address when same as registration
disableWhen(path.residenceAddress, path.sameAsRegistration, (same) => same === true);

// Behavior: Disable residence address when same as registration
disableWhen(path.residenceAddress, path.sameAsRegistration, (same) => same === true);

// Validation: Require residence address when different
applyWhen(
path.sameAsRegistration,
(same) => !same,
(p) => {
required(p.residenceAddress.city, { message: 'City is required' });
}
);

Perfect synchronization:

  • When sameAsRegistration = true β†’ Field hidden and not required
  • When sameAsRegistration = false β†’ Field visible and required

Testing the Validation​

Test these scenarios:

Phone Validation​

  • Leave main phone empty β†’ Error shown
  • Enter invalid main phone format β†’ Error shown
  • Enter valid main phone β†’ No error
  • Enter invalid additional phone β†’ Error shown (even though optional)
  • Leave additional phone empty β†’ No error

Email Validation​

  • Leave main email empty β†’ Error shown
  • Enter invalid email format (no @) β†’ Error shown
  • Enter invalid email format (no domain) β†’ Error shown
  • Enter valid email β†’ No error
  • Enter invalid additional email β†’ Error shown (even though optional)
  • Leave additional email empty β†’ No error

Registration Address​

  • Leave city empty β†’ Error shown
  • Leave street empty β†’ Error shown
  • Leave house empty β†’ Error shown
  • Leave apartment empty β†’ No error (optional)
  • Enter invalid postal code (5 digits) β†’ Error shown
  • Enter invalid postal code (letters) β†’ Error shown
  • Enter valid postal code (6 digits) β†’ No error
  • Leave postal code empty β†’ No error (optional)

Residence Address​

  • Check "same as registration" β†’ Residence fields not required
  • Uncheck "same as registration" β†’ Residence fields become required
  • Leave residence city empty (when different) β†’ Error shown
  • Leave residence street empty (when different) β†’ Error shown
  • Leave residence house empty (when different) β†’ Error shown

Supported Phone Formats​

The phone() validator accepts various formats:

// All valid formats:
'+7 (999) 123-45-67';
'+79991234567';
'89991234567';
'9991234567';
'+1 (234) 567-8901'; // International

Supported Email Formats​

The email() validator follows standard email format:

// Valid emails:
'user@example.com';
'user.name@example.com';
'user+tag@example.co.uk';
'user_name123@sub.example.org';

// Invalid emails:
'user@'; // No domain
'@example.com'; // No user
'user @example.com'; // Space in username
'user@example'; // No TLD

Key Takeaways​

  1. Format Validators - Use built-in email() and phone() for formats
  2. Separate Required - Format validators skip empty values
  3. Conditional Required - Use applyWhen() for dynamic requirements
  4. Works with Behaviors - Hidden/disabled fields skip validation
  5. Optional Validation - Can validate format even when not required

Common Patterns​

Required Email​

required(path.email, { message: 'Email is required' });
email(path.email, { message: 'Invalid email format' });

Optional Email (validates format if provided)​

email(path.emailAdditional, { message: 'Invalid email format' });
// No required() - field is optional

Russian Postal Code​

pattern(path.postalCode, /^\d{6}$/, {
message: 'Postal code must be 6 digits',
});

What's Next?​

In the next section, we'll add validation for Step 4: Employment, including:

  • Required employment status
  • Conditional validation for employed vs self-employed
  • Income validation with minimum thresholds
  • Work experience validation
  • Business-specific field validation

We'll continue using the conditional validation patterns learned here!