Cross-Step Behaviors
Coordinating behaviors across multiple form steps.
Overviewβ
Some behaviors need data from multiple steps. These cross-step behaviors handle:
- Payment-to-Income Ratio - Uses Step 1 (payment) and Step 4/5 (income)
- Smart Revalidation - Triggers validation when dependencies change
- Age-Based Access Control - Uses Step 2 (age) to control Step 1 (loan fields)
- Analytics Tracking - Monitors user behavior across the form
Why Separate Cross-Step Behaviors?β
Benefits of separation:
- Clarity - Easy to see which behaviors span multiple steps
- Maintainability - Changes to step behaviors don't affect cross-step logic
- Documentation - Cross-step dependencies are explicit
Implementationβ
reformer-tutorial/src/forms/credit-application/schemas/behaviors/cross-step.behaviors.ts
import { computeFrom, disableWhen, revalidateWhen, watchField } from '@reformer/core/behaviors';
import type { BehaviorSchemaFn } from '@reformer/core/behaviors';
import type { FieldPath } from '@reformer/core';
import type { CreditApplicationForm } from '@/types';
export const crossStepBehaviorsSchema: BehaviorSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
// ==========================================
// 1. Payment-to-Income Ratio
// Step 1: monthlyPayment
// Step 4: totalIncome
// Step 5: coBorrowersIncome
// ==========================================
computeFrom(
[path.monthlyPayment, path.totalIncome, path.coBorrowersIncome],
path.paymentToIncomeRatio,
(values) => {
const payment = values.monthlyPayment as number;
const mainIncome = values.totalIncome as number;
const coIncome = values.coBorrowersIncome as number;
const totalHouseholdIncome = (mainIncome || 0) + (coIncome || 0);
if (!totalHouseholdIncome || !payment) return 0;
return Math.round((payment / totalHouseholdIncome) * 100);
}
);
// Disable paymentToIncomeRatio (read-only)
disableWhen(path.paymentToIncomeRatio, () => true);
// ==========================================
// 2. Revalidate Payment When Income Changes
// Validation checks if payment <= 50% of income
// ==========================================
revalidateWhen(path.monthlyPayment, [path.totalIncome, path.coBorrowersIncome]);
// ==========================================
// 3. Age-Based Access Control
// Step 2: age
// Step 1: loan fields
// ==========================================
disableWhen(path.loanAmount, (form) => (form.age as number) < 18);
disableWhen(path.loanTerm, (form) => (form.age as number) < 18);
disableWhen(path.loanPurpose, (form) => (form.age as number) < 18);
// ==========================================
// 4. Analytics Tracking
// ==========================================
watchField(path.loanAmount, (value) => {
console.log('Loan amount changed:', value);
// window.analytics?.track('loan_amount_changed', { amount: value });
});
watchField(path.interestRate, (value) => {
console.log('Interest rate computed:', value);
// window.analytics?.track('interest_rate_computed', { rate: value });
});
watchField(path.employmentStatus, (value) => {
console.log('Employment status changed:', value);
// window.analytics?.track('employment_status_changed', { status: value });
});
};
Understanding Each Behaviorβ
1. Payment-to-Income Ratioβ
This is a critical metric for loan approval:
- Input: Monthly payment, applicant income, co-borrowers income
- Output: Percentage (e.g., 35% means payment is 35% of income)
- Use: Banks typically require ratio < 50%
Dependency chain:
loanAmount, loanTerm, interestRate
β
monthlyPayment (Step 1)
β
paymentToIncomeRatio β totalIncome (Step 4)
β coBorrowersIncome (Step 5)
2. Smart Revalidationβ
When income changes, we need to revalidate the payment:
// Validation rule (implemented in Validation section)
validate(path.monthlyPayment, (payment, ctx) => {
const totalIncome = ctx.form.totalIncome.value.value || 0;
const coBorrowersIncome = ctx.form.coBorrowersIncome.value.value || 0;
const total = totalIncome + coBorrowersIncome;
if (payment > total * 0.5) {
return { code: 'maxPaymentToIncome', message: 'Payment exceeds 50% of income' };
}
return null;
});
// Behavior: Trigger revalidation when income changes
revalidateWhen(path.monthlyPayment, [path.totalIncome, path.coBorrowersIncome]);
Why needed:
- User fills loan info first (Step 1)
- Then fills income (Step 4)
- Payment validation should run again with new income data
- Without
revalidateWhen, validation only runs when payment changes
3. Age-Based Access Controlβ
Prevent minors from applying for loans:
disableWhen(path.loanAmount, (form) => (form.age as number) < 18);
Flow:
- User enters birth date (Step 2)
- Age is computed automatically
- If age < 18, loan fields in Step 1 become disabled
- User cannot proceed with application
This demonstrates backward dependencies - Step 2 data affects Step 1 UI.
4. Analytics Trackingβ
Monitor user behavior for insights:
watchField(path.loanAmount, (value) => {
// Track loan amount changes
window.analytics?.track('loan_amount_changed', { amount: value });
});
Use cases:
- Track which loan types are most popular
- Monitor interest rate distribution
- Analyze drop-off points in the form
- A/B testing different form flows
Production Analytics
In production, integrate with your analytics platform:
import { analytics } from '@/services/analytics';
watchField(path.loanAmount, (value) => {
analytics.track('LoanAmountChanged', {
amount: value,
timestamp: Date.now(),
sessionId: getSessionId(),
});
});
Complete Codeβ
reformer-tutorial/src/forms/credit-application/schemas/behaviors/cross-step.behaviors.ts
import { computeFrom, disableWhen, revalidateWhen, watchField } from '@reformer/core/behaviors';
import type { BehaviorSchemaFn } from '@reformer/core/behaviors';
import type { FieldPath } from '@reformer/core';
import type { CreditApplicationForm } from '@/types';
export const crossStepBehaviorsSchema: BehaviorSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
// Payment-to-Income Ratio
computeFrom(
[path.monthlyPayment, path.totalIncome, path.coBorrowersIncome],
path.paymentToIncomeRatio,
(values) => {
const payment = values.monthlyPayment as number;
const mainIncome = values.totalIncome as number;
const coIncome = values.coBorrowersIncome as number;
const totalHouseholdIncome = (mainIncome || 0) + (coIncome || 0);
if (!totalHouseholdIncome || !payment) return 0;
return Math.round((payment / totalHouseholdIncome) * 100);
}
);
disableWhen(path.paymentToIncomeRatio, () => true);
// Smart Revalidation
revalidateWhen(path.monthlyPayment, [path.totalIncome, path.coBorrowersIncome]);
// Age-Based Access Control
disableWhen(path.loanAmount, (form) => (form.age as number) < 18);
disableWhen(path.loanTerm, (form) => (form.age as number) < 18);
disableWhen(path.loanPurpose, (form) => (form.age as number) < 18);
// Analytics Tracking
watchField(path.loanAmount, (value) => {
console.log('Loan amount changed:', value);
});
watchField(path.interestRate, (value) => {
console.log('Interest rate computed:', value);
});
watchField(path.employmentStatus, (value) => {
console.log('Employment status changed:', value);
});
};
Displaying Cross-Step Dataβ
Show the payment-to-income ratio in a summary widget:
src/components/LoanSummary.tsx
import { useFormControl } from '@reformer/core';
function LoanSummary({ control }: Props) {
const { value: monthlyPayment } = useFormControl(control.monthlyPayment);
const { value: paymentToIncomeRatio } = useFormControl(control.paymentToIncomeRatio);
const isAcceptable = paymentToIncomeRatio <= 50;
return (
<div className="p-4 bg-gray-50 rounded">
<h3 className="font-semibold mb-2">Loan Summary</h3>
<div className="flex justify-between mb-2">
<span>Monthly Payment:</span>
<span className="font-bold">{monthlyPayment.toLocaleString()} β½</span>
</div>
<div className="flex justify-between">
<span>Payment to Income:</span>
<span className={`font-bold ${isAcceptable ? 'text-green-600' : 'text-red-600'}`}>
{paymentToIncomeRatio}%
</span>
</div>
{!isAcceptable && (
<p className="text-sm text-red-600 mt-2">
Payment exceeds 50% of household income. Consider: - Reducing loan amount - Extending loan
term - Adding co-borrowers
</p>
)}
</div>
);
}
Resultβ
Cross-step behaviors now provide:
- β Payment-to-income ratio calculation
- β Smart revalidation on income changes
- β Age-based access control (prevents minors from applying)
- β Analytics tracking for insights
Key Takeawaysβ
- Separate cross-step behaviors for clarity
revalidateWhenensures validation stays current- Backward dependencies are possible (Step 2 β Step 1)
- Analytics via
watchFieldfor monitoring - Display cross-step data in summaries/widgets
Next Stepβ
Now let's combine all behaviors and register them with the form in the final section.