@matthew.ngo/reform
Version:
A flexible and powerful React form management library with advanced validation, state observation, and multi-group support
385 lines (322 loc) • 10.3 kB
Markdown
# Reform Form Watcher
Reform provides a powerful form watching system that allows you to observe and react to changes in form fields and form state. This document covers the form watching capabilities of Reform.
## Table of Contents
- [Overview](#overview)
- [Field Watching](#field-watching)
- [Form State Observation](#form-state-observation)
- [Combined Capabilities](#combined-capabilities)
- [Auto-Save Implementation](#auto-save-implementation)
- [API Reference](#api-reference)
## Overview
Reform's form watcher system allows you to:
1. **Watch specific fields** for changes with debounce support
2. **Observe form state** changes (dirty, valid, errors, etc.)
3. **Combine field and state watching** for advanced use cases
4. **Create auto-save functionality** with minimal code
## Field Watching
Watch specific fields for changes and react accordingly:
```tsx
import { useReform, useReformWatcher } from '@reform/core';
type UserForm = {
email: string;
username: string;
password: string;
};
const UserFormComponent = () => {
const form = useReform<UserForm>({
defaultData: {
email: '',
username: '',
password: '',
},
minGroups: 1,
});
// Initialize the watcher
const watcher = useReformWatcher(form);
// Set up field watchers
useEffect(() => {
// Watch email field with debounce
const unsubscribeEmail = watcher.watchField({
field: 'email',
callback: async (value, prevValue, groupIndex) => {
if (value && value !== prevValue) {
const isAvailable = await checkEmailAvailability(value);
if (!isAvailable) {
form.setError(groupIndex, 'email', {
type: 'manual',
message: 'Email is already taken',
});
} else {
form.clearErrors(groupIndex, 'email');
}
}
},
debounce: 500, // Wait 500ms after typing stops
});
// Watch username field
const unsubscribeUsername = watcher.watchField({
field: 'username',
callback: (value) => {
console.log('Username changed:', value);
},
});
// Clean up watchers when component unmounts
return () => {
unsubscribeEmail();
unsubscribeUsername();
};
}, [watcher, form]);
return (
<form>
<div>
<label>Email</label>
<input {...form.register(0, 'email')} />
{form.getFieldError(0, 'email') && (
<p className="error">{form.getFieldError(0, 'email')?.message}</p>
)}
</div>
<div>
<label>Username</label>
<input {...form.register(0, 'username')} />
</div>
<div>
<label>Password</label>
<input type="password" {...form.register(0, 'password')} />
</div>
</form>
);
};
```
## Form State Observation
Observe changes in form state:
```tsx
import { useReform, useReformWatcher } from '@reform/core';
const FormWithStateObservation = () => {
const form = useReform<UserForm>(config);
const [formStatus, setFormStatus] = useState('');
// Initialize the watcher with state change callbacks
const watcher = useReformWatcher(form, {
onDirtyChange: (isDirty) => {
console.log('Form dirty state changed:', isDirty);
},
onErrorsChange: (hasErrors, errors) => {
console.log('Form errors changed:', hasErrors, errors);
},
onValidChange: (isValid) => {
console.log('Form validity changed:', isValid);
},
});
// Subscribe to form state changes
useEffect(() => {
const subscription = watcher.subscribeToState((state, prevState) => {
if (state.isDirty && !prevState?.isDirty) {
setFormStatus('Form has unsaved changes');
} else if (state.isSubmitting) {
setFormStatus('Submitting form...');
} else if (state.isSubmitSuccessful) {
setFormStatus('Form submitted successfully!');
} else if (Object.keys(state.errors).length > 0) {
setFormStatus('Form has errors');
} else if (state.isValid) {
setFormStatus('Form is valid');
} else {
setFormStatus('');
}
});
return subscription.unsubscribe;
}, [watcher]);
return (
<div>
<form>{/* Form fields */}</form>
{formStatus && <div className="form-status">{formStatus}</div>}
<div className="form-state-info">
<p>Is Dirty: {watcher.isDirty() ? 'Yes' : 'No'}</p>
<p>Is Valid: {watcher.isValid() ? 'Yes' : 'No'}</p>
<p>Has Errors: {watcher.hasErrors() ? 'Yes' : 'No'}</p>
<p>Is Submitting: {watcher.isSubmitting() ? 'Yes' : 'No'}</p>
</div>
</div>
);
};
```
## Combined Capabilities
Combine field watching with form state observation:
```tsx
import { useReform, useReformWatcher } from '@reform/core';
const CombinedWatcherForm = () => {
const form = useReform<UserForm>(config);
const watcher = useReformWatcher(form);
useEffect(() => {
// Watch a field and also react to form state changes
const unsubscribe = watcher.watchFieldWithState(
{
field: 'email',
callback: (value) => {
console.log('Email changed:', value);
},
},
(state) => {
if (state.isSubmitting) {
console.log('Form is submitting with email:', form.getFieldValue(0, 'email'));
}
}
);
return unsubscribe;
}, [watcher, form]);
// Watch a field only when the form is valid
useEffect(() => {
const unsubscribe = watcher.watchFieldWhenValid({
field: 'username',
callback: (value) => {
// This will only execute when the form is valid
console.log('Valid username change:', value);
},
});
return unsubscribe;
}, [watcher]);
// Watch for field errors
useEffect(() => {
const unsubscribe = watcher.watchFieldErrors('password', (hasError, errorMessage) => {
if (hasError) {
console.log('Password error:', errorMessage);
} else {
console.log('Password is valid');
}
});
return unsubscribe;
}, [watcher]);
return (
<form>{/* Form fields */}</form>
);
};
```
## Auto-Save Implementation
Create an auto-save feature with the watcher:
```tsx
import { useReform, useReformWatcher } from '@reform/core';
const AutoSaveForm = () => {
const form = useReform<UserForm>(config);
const watcher = useReformWatcher(form);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
// Set up auto-save
useEffect(() => {
const saveForm = async () => {
try {
await saveFormToServer(form.getGroups());
setLastSaved(new Date());
} catch (error) {
console.error('Auto-save failed:', error);
}
};
// Create auto-save controller
const autoSave = watcher.createAutoSave(saveForm, 2000);
// Clean up
return () => {
autoSave.unsubscribe();
};
}, [watcher, form]);
// Toggle auto-save
const toggleAutoSave = () => {
if (autoSaveEnabled) {
autoSave.stop();
} else {
autoSave.start();
}
setAutoSaveEnabled(!autoSaveEnabled);
};
// Manually trigger save
const handleManualSave = () => {
autoSave.trigger();
};
return (
<div>
<form>{/* Form fields */}</form>
<div className="auto-save-controls">
<label>
<input
type="checkbox"
checked={autoSaveEnabled}
onChange={toggleAutoSave}
/>
Enable auto-save
</label>
<button onClick={handleManualSave}>
Save Now
</button>
{lastSaved && (
<div className="last-saved">
Last saved: {lastSaved.toLocaleString()}
</div>
)}
</div>
</div>
);
};
```
## API Reference
### useReformWatcher
```typescript
function useReformWatcher<T extends Record<string, any>>(
reform: ReformReturn<T>,
options: FormStateObserverOptions<T> = {}
): ReformWatcherReturn<T>
```
#### Options
| Option | Type | Description |
|--------|------|-------------|
| onChange | `FormStateChangeHandler<T>` | Called on any form state change |
| onDirtyChange | `(isDirty: boolean) => void` | Called when dirty state changes |
| onErrorsChange | `(hasErrors: boolean, errors: any) => void` | Called when errors change |
| onSubmitCountChange | `(count: number) => void` | Called when submit count changes |
| onTouchedChange | `(hasTouched: boolean, touched: any) => void` | Called when touched fields change |
| onValidChange | `(isValid: boolean) => void` | Called when validity changes |
| onSubmittingChange | `(isSubmitting: boolean) => void` | Called when submitting state changes |
### Field Watching Methods
| Method | Description |
|--------|-------------|
| watchField | Watch a specific field for changes |
| watchFields | Watch multiple fields at once |
| getFieldValue | Get the current value of a field |
| triggerFieldWatch | Manually trigger field watchers |
### Form State Methods
| Method | Description |
|--------|-------------|
| subscribeToState | Subscribe to form state changes |
| getFormState | Get current form state |
| getPreviousState | Get previous form state |
| isDirty | Check if form is dirty |
| hasErrors | Check if form has errors |
| isSubmitting | Check if form is submitting |
| isValid | Check if form is valid |
### Combined Methods
| Method | Description |
|--------|-------------|
| watchFieldWithState | Watch a field and form state together |
| watchFieldWhenValid | Watch a field only when form is valid |
| watchFieldErrors | Watch for errors in a specific field |
| createAutoSave | Create an auto-save controller |
### FieldWatchConfig
```typescript
interface FieldWatchConfig<T, K extends keyof T> {
field: K;
callback: (
value: T[K],
previousValue: T[K],
groupIndex: number,
formData: { groups: FormGroup<T>[] }
) => void;
groupIndex?: number;
debounce?: number;
immediate?: boolean;
}
```
### AutoSaveController
```typescript
interface AutoSaveController {
stop: () => void;
start: () => void;
trigger: () => void;
unsubscribe: () => void;
}
```