@matthew.ngo/reform
Version:
A flexible and powerful React form management library with advanced validation, state observation, and multi-group support
327 lines (326 loc) • 11.6 kB
TypeScript
/// <reference types="react" />
/**
* Defines types and interfaces for conditional field rendering and validation.
* This module provides the type definitions for dynamically showing, hiding,
* and validating fields based on form values.
*/
import { UseFormRegisterReturn } from 'react-hook-form';
import { FormGroup } from '../../core/form/form-groups';
/**
* Base props for conditional rendering components
* Contains properties to determine visibility and validation status
*/
export interface ConditionalRenderProps {
/** Whether the field or component should be displayed */
shouldShow: boolean;
/** Whether validation should be applied even when hidden */
shouldValidate: boolean;
/**
* Helper function to conditionally render a component
*
* @param component - The React component to conditionally render
* @returns The component if condition is met, otherwise null
*
* @example
* // Render a field only when user selects "Other" option
* const { render } = when(groupIndex, data => data.selection === 'other');
*
* return (
* <>
* <SelectField name="selection" options={options} />
* {render(<TextField name="otherDetails" label="Please specify" />)}
* </>
* );
*/
render: (component: React.ReactNode) => React.ReactNode | null;
}
/**
* Extended conditional rendering props for group-specific conditions
* Includes methods for conditional field registration and validation
*
* @template T - The type of form data
*/
export interface GroupConditionalRenderProps<T> extends ConditionalRenderProps {
/**
* Conditionally registers a field with React Hook Form
* If the condition is not met, either returns a dummy register object
* or clears the field value based on options
*
* @template K - The key of the field to register
* @param fieldName - The name of the field to register
* @param options - Registration options passed to React Hook Form
* @returns Either a real or dummy registration object
*
* @example
* // Register a field only when the user is employed
* const { registerIf } = when(groupIndex, data => data.employmentStatus === 'employed');
*
* return (
* <input
* {...registerIf('employerName', { required: 'Employer name is required' })}
* placeholder="Employer name"
* />
* );
*/
registerIf: <K extends keyof T>(fieldName: K, options?: any) => UseFormRegisterReturn | {
name: string;
onChange: () => void;
onBlur: () => void;
ref: () => void;
};
/**
* Conditionally validates a field based on the condition
* If the condition is not met and validateWhenHidden is false,
* validation is skipped
*
* @template K - The key of the field to validate
* @param fieldName - The name of the field to validate
* @param validationFn - Function that validates the field value
* @returns Validation error message or undefined if valid/skipped
*
* @example
* // Validate a field only when a checkbox is checked
* const { validateIf } = when(groupIndex, data => data.needsVerification);
*
* const error = validateIf('verificationCode',
* value => value?.length !== 6 ? 'Code must be 6 digits' : undefined
* );
*
* if (error) {
* setError(error);
* }
*/
validateIf: <K extends keyof T>(fieldName: K, validationFn: (value: T[K]) => string | undefined) => string | undefined;
}
/**
* Interface for managing conditional fields within a form
* Provides methods to show/hide fields based on conditions
*
* @template T - The type of form data
*/
export interface ConditionalFieldsManager<T> {
/**
* Creates a condition based on values within a specific group
*
* @param groupIndex - The index of the group to evaluate
* @param condition - Function that determines if condition is met
* @param options - Configuration options for hidden fields
* @returns Object with conditional rendering and validation helpers
*
* @example
* // Show additional fields based on selection
* const { when } = useReform(formConfig);
*
* const { render, registerIf } = when(
* 0, // first group
* data => data.hasChildren === true,
* { clearWhenHidden: true }
* );
*
* return (
* <form>
* <Checkbox {...register('hasChildren')} label="Do you have children?" />
*
* {render(
* <div>
* <NumberInput
* {...registerIf('childrenCount', { min: 1 })}
* label="How many children?"
* />
* </div>
* )}
* </form>
* );
*
* @example
* // Show payment details only when payment method is credit card
* const { render, registerIf } = when(
* 0,
* data => data.paymentMethod === 'creditCard',
* { clearWhenHidden: true }
* );
*
* return (
* <div>
* <SelectField
* name="paymentMethod"
* options={['creditCard', 'bankTransfer', 'paypal']}
* />
*
* {render(
* <div className="credit-card-fields">
* <TextField {...registerIf('cardNumber')} label="Card Number" />
* <TextField {...registerIf('cardName')} label="Name on Card" />
* <div className="flex">
* <TextField {...registerIf('expiryDate')} label="Expiry Date" />
* <TextField {...registerIf('cvv')} label="CVV" />
* </div>
* </div>
* )}
* </div>
* );
*/
when: (groupIndex: number, condition: (groupData: T) => boolean, options?: {
/** Whether to clear field values when hidden (default: true) */
clearWhenHidden?: boolean;
/** Whether to validate fields even when hidden (default: false) */
validateWhenHidden?: boolean;
}) => GroupConditionalRenderProps<T>;
/**
* Creates a condition based on values across all groups
*
* @param condition - Function that determines if condition is met
* @param options - Configuration options for hidden fields
* @returns Object with conditional rendering helpers
*
* @example
* // Show a summary section only when all groups have data
* const { whenAny } = useReform(formConfig);
*
* const { render } = whenAny(
* groups => groups.every(group =>
* group.data.firstName && group.data.lastName
* )
* );
*
* return (
* <>
* {render(
* <SummarySection groups={groups} />
* )}
* </>
* );
*
* @example
* // Show summary section only when all required fields are filled
* const { render } = whenAny(groups => {
* return groups.every(group =>
* group.data.firstName &&
* group.data.lastName &&
* group.data.email
* );
* });
*
* return (
* <div>
* {render(
* <div className="summary-panel">
* <h2>Summary</h2>
* <SummaryTable data={groups} />
* <SubmitButton />
* </div>
* )}
* </div>
* );
*/
whenAny: (condition: (allGroups: FormGroup<T>[]) => boolean, options?: {
/** Whether to clear field values when hidden (default: true) */
clearWhenHidden?: boolean;
/** Whether to validate fields even when hidden (default: false) */
validateWhenHidden?: boolean;
}) => ConditionalRenderProps;
/**
* Creates a condition that depends on values from multiple groups and the entire form state
*
* @param condition - Function that evaluates condition based on all form data
* @returns Object with conditional rendering helpers
*
* @example
* // Show a component only when total participants across all groups exceeds 10
* const { whenCross } = useReform(formConfig);
*
* const { render } = whenCross(formData => {
* const totalParticipants = formData.groups.reduce(
* (sum, group) => sum + (group.data.participants || 0),
* 0
* );
* return totalParticipants > 10;
* });
*
* return (
* <div>
* {render(
* <div className="large-group-notice">
* <Alert severity="info">
* For groups larger than 10, please contact our group sales department.
* </Alert>
* <Button>Contact Sales</Button>
* </div>
* )}
* </div>
* );
*
* @example
* // Show discount information when order total exceeds threshold
* const { render } = whenCross(formData => {
* const orderTotal = formData.groups.reduce((sum, group) => {
* return sum + calculateGroupTotal(group.data);
* }, 0);
*
* return orderTotal > 1000;
* });
*
* return (
* <>
* {render(
* <DiscountBanner message="You qualify for a 10% bulk discount!" />
* )}
* </>
* );
*/
whenCross: (condition: (formData: {
groups: FormGroup<T>[];
}) => boolean) => ConditionalRenderProps;
/**
* Creates a dependency tracking function that runs effects when specific fields change
*
* @template K - The keys of fields to track
* @param groupIndex - The index of the group containing the fields
* @param dependencies - Array of field names to track
* @param effect - Function to run when dependencies change
*
* @example
* // Update total price when quantity or price changes
* const { createDependencyTracker } = useReform(formConfig);
*
* createDependencyTracker(
* groupIndex,
* ['quantity', 'unitPrice'],
* ({ quantity, unitPrice }) => {
* const total = (quantity || 0) * (unitPrice || 0);
* setValue(groupIndex, 'totalPrice', total);
* }
* );
*
* @example
* // Calculate total when quantity or price changes
* createDependencyTracker(
* 0,
* ['quantity', 'price'],
* ({ quantity, price }) => {
* const total = (quantity || 0) * (price || 0);
* setValue(0, 'total', total);
* }
* );
*
* @example
* // Update shipping options based on country selection
* createDependencyTracker(
* groupIndex,
* ['country'],
* ({ country }) => {
* if (country) {
* const shippingOptions = getShippingOptionsForCountry(country);
* setShippingOptions(shippingOptions);
*
* // Reset shipping method if not available in new country
* const currentMethod = getValue(groupIndex, 'shippingMethod');
* if (currentMethod && !shippingOptions.includes(currentMethod)) {
* setValue(groupIndex, 'shippingMethod', '');
* }
* }
* }
* );
*/
createDependencyTracker: <K extends keyof T>(groupIndex: number, dependencies: K[], effect: (values: Pick<T, K>) => void) => void;
}