@matthew.ngo/reform
Version:
A flexible and powerful React form management library with advanced validation, state observation, and multi-group support
404 lines (345 loc) • 9.82 kB
Markdown
# Reform Conditional Fields
Reform provides a powerful conditional fields system that allows you to show or hide form fields based on the values of other fields. This document covers the conditional fields capabilities of Reform.
## Table of Contents
- [Overview](#overview)
- [Basic Conditional Rendering](#basic-conditional-rendering)
- [Conditional Field Registration](#conditional-field-registration)
- [Cross-Group Conditions](#cross-group-conditions)
- [Dependency Tracking](#dependency-tracking)
- [API Reference](#api-reference)
## Overview
Reform's conditional fields system allows you to:
1. **Conditionally render** form fields or components based on form values
2. **Conditionally register** fields with React Hook Form
3. **Conditionally validate** fields based on other field values
4. **Track dependencies** between fields and run effects when values change
## Basic Conditional Rendering
The most common use case is to show or hide fields based on the value of another field:
```tsx
import { useReform } from '@reform/core';
type UserForm = {
employmentStatus: 'employed' | 'self-employed' | 'unemployed';
employerName: string;
businessName: string;
};
const MyForm = () => {
const form = useReform<UserForm>({
defaultData: {
employmentStatus: 'unemployed',
employerName: '',
businessName: ''
},
minGroups: 1,
});
// Get conditional rendering props for employed status
const employedCondition = form.when(
0,
data => data.employmentStatus === 'employed'
);
// Get conditional rendering props for self-employed status
const selfEmployedCondition = form.when(
0,
data => data.employmentStatus === 'self-employed'
);
return (
<form>
<select {...form.register(0, 'employmentStatus')}>
<option value="employed">Employed</option>
<option value="self-employed">Self-employed</option>
<option value="unemployed">Unemployed</option>
</select>
{/* Render employer field only when employed */}
{employedCondition.render(
<div>
<label>Employer Name</label>
<input {...form.register(0, 'employerName')} />
</div>
)}
{/* Render business field only when self-employed */}
{selfEmployedCondition.render(
<div>
<label>Business Name</label>
<input {...form.register(0, 'businessName')} />
</div>
)}
</form>
);
};
```
## Conditional Field Registration
For more control, you can conditionally register fields:
```tsx
import { useReform } from '@reform/core';
type PaymentForm = {
paymentMethod: 'creditCard' | 'bankTransfer' | 'paypal';
cardNumber: string;
cardName: string;
expiryDate: string;
cvv: string;
};
const PaymentForm = () => {
const form = useReform<PaymentForm>({
defaultData: {
paymentMethod: 'bankTransfer',
cardNumber: '',
cardName: '',
expiryDate: '',
cvv: ''
},
minGroups: 1,
});
// Get conditional rendering and registration props
const { render, registerIf } = form.when(
0,
data => data.paymentMethod === 'creditCard',
{ clearWhenHidden: true } // Clear field values when hidden
);
return (
<form>
<select {...form.register(0, 'paymentMethod')}>
<option value="creditCard">Credit Card</option>
<option value="bankTransfer">Bank Transfer</option>
<option value="paypal">PayPal</option>
</select>
{render(
<div className="credit-card-fields">
<div>
<label>Card Number</label>
<input
{...registerIf('cardNumber', {
required: 'Card number is required'
})}
/>
</div>
<div>
<label>Name on Card</label>
<input
{...registerIf('cardName', {
required: 'Name on card is required'
})}
/>
</div>
<div className="flex">
<div>
<label>Expiry Date</label>
<input
{...registerIf('expiryDate', {
required: 'Expiry date is required'
})}
/>
</div>
<div>
<label>CVV</label>
<input
{...registerIf('cvv', {
required: 'CVV is required',
pattern: {
value: /^\d{3,4}$/,
message: 'CVV must be 3 or 4 digits'
}
})}
/>
</div>
</div>
</div>
)}
</form>
);
};
```
## Cross-Group Conditions
Reform allows you to create conditions based on values across all groups:
```tsx
import { useReform } from '@reform/core';
type PersonForm = {
firstName: string;
lastName: string;
email: string;
};
const FamilyForm = () => {
const form = useReform<PersonForm>({
defaultData: { firstName: '', lastName: '', email: '' },
minGroups: 1,
});
// Add a new person
const handleAddPerson = () => {
form.addGroup();
};
// Show summary only when all required fields are filled
const { render } = form.whenAny(groups => {
return groups.every(group =>
group.data.firstName &&
group.data.lastName &&
group.data.email
);
});
// Show discount message when more than 3 people
const { render: renderDiscount } = form.whenCross(formData => {
return formData.groups.length > 3;
});
return (
<div>
{form.getGroups().map((group, index) => (
<div key={group.id} className="person-form">
<h3>Person {index + 1}</h3>
<input
{...form.register(index, 'firstName')}
placeholder="First Name"
/>
<input
{...form.register(index, 'lastName')}
placeholder="Last Name"
/>
<input
{...form.register(index, 'email')}
placeholder="Email"
/>
</div>
))}
<button type="button" onClick={handleAddPerson}>
Add Person
</button>
{renderDiscount(
<div className="discount-message">
Family discount applied for groups larger than 3!
</div>
)}
{render(
<div className="summary-panel">
<h2>Summary</h2>
<table>
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{form.getGroups().map((group, index) => (
<tr key={group.id}>
<td>{group.data.firstName}</td>
<td>{group.data.lastName}</td>
<td>{group.data.email}</td>
</tr>
))}
</tbody>
</table>
<button type="submit">Submit</button>
</div>
)}
</div>
);
};
```
## Dependency Tracking
Reform allows you to track dependencies between fields and run effects when values change:
```tsx
import { useReform } from '@reform/core';
import { useEffect } from 'react';
type OrderForm = {
quantity: number;
unitPrice: number;
totalPrice: number;
};
const OrderForm = () => {
const form = useReform<OrderForm>({
defaultData: { quantity: 1, unitPrice: 0, totalPrice: 0 },
minGroups: 1,
});
// Set up dependency tracking
useEffect(() => {
form.createDependencyTracker(
0,
['quantity', 'unitPrice'],
({ quantity, unitPrice }) => {
const total = (quantity || 0) * (unitPrice || 0);
form.setValue(0, 'totalPrice', total);
}
);
}, []);
return (
<form>
<div>
<label>Quantity</label>
<input
type="number"
{...form.register(0, 'quantity')}
min="1"
/>
</div>
<div>
<label>Unit Price</label>
<input
type="number"
{...form.register(0, 'unitPrice')}
step="0.01"
/>
</div>
<div>
<label>Total Price</label>
<input
type="number"
{...form.register(0, 'totalPrice')}
readOnly
/>
</div>
</form>
);
};
```
## API Reference
### ConditionalFieldsManager
```typescript
interface ConditionalFieldsManager<T> {
when(
groupIndex: number,
condition: (groupData: T) => boolean,
options?: {
clearWhenHidden?: boolean;
validateWhenHidden?: boolean;
}
): GroupConditionalRenderProps<T>;
whenAny(
condition: (allGroups: FormGroup<T>[]) => boolean,
options?: {
clearWhenHidden?: boolean;
validateWhenHidden?: boolean;
}
): ConditionalRenderProps;
whenCross(
condition: (formData: { groups: FormGroup<T>[] }) => boolean
): ConditionalRenderProps;
createDependencyTracker<K extends keyof T>(
groupIndex: number,
dependencies: K[],
effect: (values: Pick<T, K>) => void
): void;
}
```
### GroupConditionalRenderProps
```typescript
interface GroupConditionalRenderProps<T> extends ConditionalRenderProps {
registerIf<K extends keyof T>(
fieldName: K,
options?: any
): UseFormRegisterReturn | {
name: string;
onChange: () => void;
onBlur: () => void;
ref: () => void;
};
validateIf<K extends keyof T>(
fieldName: K,
validationFn: (value: T[K]) => string | undefined
): string | undefined;
}
```
### ConditionalRenderProps
```typescript
interface ConditionalRenderProps {
shouldShow: boolean;
shouldValidate: boolean;
render: (component: React.ReactNode) => React.ReactNode | null;
}
```