Step 2: Personal Information Behaviors
Auto-generating full name and calculating age from personal data.
Overview
For Step 2 (Personal Information), we need simpler behaviors that derive values from the personalData group:
- Computed: Full Name - Generate from first, last, and middle names (Russian FIO format)
- Computed: Age - Calculate from birth date
- Disable: Computed Fields - Make them read-only
These computed fields will be displayed in other parts of the form and used in validation/submission logic.
Creating the Behavior File
Create the behavior file for Step 2:
touch reformer-tutorial/src/forms/credit-application/schemas/behaviors/personal-info.ts
Implementing the Behaviors
1. Full Name Computation
In Russian forms, the full name (ФИО) is typically formatted as: Фамилия Имя Отчество (Last First Middle).
import { computeFrom, disableWhen } from '@reformer/core/behaviors';
import type { BehaviorSchemaFn, FieldPath } from '@reformer/core';
import type { CreditApplicationForm, PersonalData } from '@/types';
export const personalBehaviorSchema: BehaviorSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
// ==========================================
// Computed: Full Name (ФИО)
// ==========================================
computeFrom([path.personalData], path.fullName, (values) => {
const pd = values.personalData as PersonalData;
if (!pd) return '';
// Russian format: Фамилия Имя Отчество
// Filter out empty values
const parts = [pd.lastName, pd.firstName, pd.middleName].filter(Boolean);
return parts.join(' ');
});
// ... more behaviors
};
How it works:
- We watch the entire
personalDatagroup (not individual fields) - When any field in
personalDatachanges, the full name updates - Empty values are filtered out (e.g., if middleName is optional)
- The result is a clean, properly formatted full name
You can watch entire groups instead of individual fields:
// ✅ Watch the entire group
computeFrom([path.personalData], ...)
// ❌ Watch individual fields (more verbose)
computeFrom([path.personalData.firstName, path.personalData.lastName, ...], ...)
Both work, but watching groups is simpler when you need all fields.
2. Age Calculation
Calculate the applicant's age from their birth date:
export const personalBehaviorSchema: BehaviorSchemaFn<CreditApplicationForm> = (path) => {
// ... previous behaviors
// ==========================================
// Computed: Age
// ==========================================
computeFrom([path.personalData], path.age, (values) => {
const birthDate = (values.personalData as PersonalData)?.birthDate;
if (!birthDate) return null;
const today = new Date();
const birth = new Date(birthDate);
// Calculate year difference
let age = today.getFullYear() - birth.getFullYear();
// Adjust if birthday hasn't occurred this year
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--;
}
return age;
});
// ... more behaviors
};
Edge cases handled:
- Returns
nullif birth date is not set - Correctly handles birthdays that haven't occurred yet this year
- Accounts for month and day differences
The age calculation checks:
- Year difference (e.g., 2025 - 1990 = 35)
- If the birthday hasn't occurred yet this year, subtract 1
- Month check: Current month < birth month → birthday not yet
- Day check: Same month, but current day < birth day → birthday not yet :::
3. Making Computed Fields Read-Only
Since fullName and age are computed automatically, they should be read-only (disabled):
export const personalBehaviorSchema: BehaviorSchemaFn<CreditApplicationForm> = (path) => {
// ... previous behaviors
// ==========================================
// Disable Computed Fields (Always Read-Only)
// ==========================================
disableWhen(path.fullName, path.fullName, () => true);
disableWhen(path.age, path.age, () => true);
};
Why disableWhen(path.fullName, path.fullName, () => true)?
- First argument: the field to disable
- Second argument: the field to watch (we watch itself)
- Third argument: condition (always
truemeans always disabled)
This pattern ensures the field is always disabled, regardless of form state.
You can also disable fields in the schema:
fullName: {
value: '',
component: Input,
componentProps: {
label: 'Full Name',
disabled: true, // ← Always disabled
},
},
However, using disableWhen keeps all behaviors centralized and makes them easier to find and modify.
Complete Code
Here's the complete behavior file for Step 2:
import { computeFrom, disableWhen } from '@reformer/core/behaviors';
import type { BehaviorSchemaFn, FieldPath } from '@reformer/core';
import type { CreditApplicationForm, PersonalData } from '@/types';
export const personalBehaviorSchema: BehaviorSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
// ==========================================
// Computed: Full Name (ФИО)
// ==========================================
computeFrom([path.personalData], path.fullName, (values) => {
const pd = values.personalData as PersonalData;
if (!pd) return '';
const parts = [pd.lastName, pd.firstName, pd.middleName].filter(Boolean);
return parts.join(' ');
});
// ==========================================
// Computed: Age
// ==========================================
computeFrom([path.personalData], path.age, (values) => {
const birthDate = (values.personalData as PersonalData)?.birthDate;
if (!birthDate) return null;
const today = new Date();
const birth = new Date(birthDate);
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--;
}
return age;
});
// ==========================================
// Disable Computed Fields
// ==========================================
disableWhen(path.fullName, path.fullName, () => true);
disableWhen(path.age, path.age, () => true);
};
Testing the Behaviors
Add Step 2 behaviors to your form temporarily:
import { loanBehaviorSchema } from '../behaviors/steps/step-1-loan-info.behaviors';
import { personalBehaviorSchema } from '../behaviors/steps/step-2-personal-info.behaviors';
export function createCreditApplicationForm() {
return createForm({
schema: creditApplicationSchema,
behaviors: (path) => {
loanBehaviorSchema(path);
personalBehaviorSchema(path); // ← Add Step 2
},
});
}
Test Scenarios
-
Full Name Generation:
- Enter first name: "Иван"
- Enter last name: "Петров"
- Enter middle name: "Сергеевич"
- Check that
fullNamefield shows: "Петров Иван Сергеевич" - Leave middle name empty → Full name should be "Петров Иван"
-
Age Calculation:
- Enter birth date: "1990-05-15"
- Check that
agefield calculates correctly - Try different dates (before/after birthday this year)
- Check that age updates when birth date changes
-
Read-Only Fields:
- Try to click on
fullNamefield → Should be disabled - Try to click on
agefield → Should be disabled - Fields should have a disabled/read-only visual state
- Try to click on
Displaying Computed Fields
These computed fields can be displayed anywhere in your form. For example, you might show them in a summary:
import { useFormControl } from '@reformer/core';
function ApplicantSummary({ control }: Props) {
const { value: fullName } = useFormControl(control.fullName);
const { value: age } = useFormControl(control.age);
return (
<div className="p-4 bg-gray-50 rounded">
<h3 className="font-semibold mb-2">Applicant Information</h3>
<div className="space-y-1 text-sm">
<div>
<span className="text-gray-600">Full Name:</span>
<span className="ml-2 font-medium">{fullName || '—'}</span>
</div>
<div>
<span className="text-gray-600">Age:</span>
<span className="ml-2 font-medium">{age ? `${age} years` : '—'}</span>
</div>
</div>
</div>
);
}
Or as read-only fields in the form:
<FormField control={control.personalData.firstName} />
<FormField control={control.personalData.lastName} />
<FormField control={control.personalData.middleName} />
<FormField control={control.personalData.birthDate} />
{/* Computed fields shown as read-only */}
<div className="grid grid-cols-2 gap-4 mt-4">
<FormField control={control.fullName} />
<FormField control={control.age} />
</div>
Result
Now Step 2 of the form has:
- ✅ Auto-generated full name in Russian FIO format
- ✅ Auto-calculated age with correct birthday handling
- ✅ Read-only display of computed fields
These computed values will be useful in:
- Display - Showing applicant info in summaries
- Validation - Age-based validation rules (e.g., must be 18+)
- Cross-step behaviors - Controlling access based on age
- Submission - Including full name in API payload
Key Takeaways
- Watch entire groups when you need all fields:
computeFrom([path.personalData], ...) - Handle edge cases in date calculations (birthdays not yet occurred)
- Use
disableWhen(..., ..., () => true)for always-disabled fields - Computed fields can be displayed anywhere in the form
- Centralize behaviors for easier maintenance
Next Step
Now let's add behaviors for Step 3: Contact Information, where we'll implement address copying and conditional visibility for the residence address.