@matthew.ngo/reform
Version:
A flexible and powerful React form management library with advanced validation, state observation, and multi-group support
213 lines (193 loc) • 6.8 kB
text/typescript
import { useMemo } from 'react';
import { ReformReturn } from '../../types';
import { useFieldWatcher } from './useFieldWatcher';
import { useFormStateObserver } from './useFormStateObserver';
import { FormStateObserverOptions } from './types';
/**
* Hook wrapper for field watching and form state observation in Reform
*
* @template T - The type of form data
* @param reform - Reform hook return value
* @param options - Configuration options for form state observer
* @returns Combined field watcher and form state observer utilities
*
* @example
* // Basic usage
* const form = useReform<UserForm>({...});
* const watcher = useReformWatcher(form);
*
* // Watch a specific field
* useEffect(() => {
* const unsubscribe = watcher.watchField({
* field: 'email',
* callback: (value) => validateEmailAsync(value),
* debounce: 500
* });
*
* return unsubscribe;
* }, [watcher]);
*
* // Subscribe to form state changes
* useEffect(() => {
* const subscription = watcher.subscribeToState((state) => {
* if (state.isDirty) {
* console.log('Form has unsaved changes');
* }
* });
*
* return subscription.unsubscribe;
* }, [watcher]);
*/
export const useReformWatcher = <T extends Record<string, any>>(
reform: ReformReturn<T>,
options: FormStateObserverOptions<T> = {}
) => {
// Extract methods from reform
const { formMethods } = reform;
const { watch, formState } = formMethods;
// Initialize field watcher
const fieldWatcher = useFieldWatcher<T>(watch);
// Initialize form state observer
const stateObserver = useFormStateObserver<T>(formState, options);
// Create a combined API
return useMemo(() => ({
// Field watching capabilities
watchField: fieldWatcher.watchField,
watchFields: fieldWatcher.watchFields,
getFieldValue: fieldWatcher.getFieldValue,
triggerFieldWatch: fieldWatcher.triggerFieldWatch,
// Form state observation capabilities
subscribeToState: stateObserver.subscribe,
getFormState: stateObserver.getState,
getPreviousState: stateObserver.getPreviousState,
isDirty: stateObserver.isDirty,
hasErrors: stateObserver.hasErrors,
isSubmitting: stateObserver.isSubmitting,
isValid: stateObserver.isValid,
// Enhanced methods that combine both capabilities
/**
* Watch a field and update when form state changes
*
* @param config - Field watch configuration
* @param stateHandler - Optional handler for form state changes
* @returns Function to unsubscribe both watchers
*/
watchFieldWithState: (
config: Parameters<typeof fieldWatcher.watchField>[0],
stateHandler?: Parameters<typeof stateObserver.subscribe>[0]
) => {
const fieldUnsubscribe = fieldWatcher.watchField(config as any);
let stateUnsubscribe = () => {};
if (stateHandler) {
const subscription = stateObserver.subscribe(stateHandler);
stateUnsubscribe = subscription.unsubscribe;
}
return () => {
fieldUnsubscribe();
stateUnsubscribe();
};
},
/**
* Watch a field only when the form is valid
*
* @param config - Field watch configuration
* @returns Function to unsubscribe
*/
watchFieldWhenValid: (
config: Parameters<typeof fieldWatcher.watchField>[0]
) => {
// Create a wrapper callback that only executes when form is valid
const originalCallback = config.callback;
const wrappedCallback: typeof originalCallback = (value, prevValue, groupIndex, formData) => {
if (stateObserver.isValid()) {
originalCallback(value, prevValue, groupIndex, formData);
}
};
// Return the field watcher with wrapped callback
return fieldWatcher.watchField({
...config,
callback: wrappedCallback
} as any);
},
/**
* Watch for changes in a specific field's error state
*
* @param field - Field to watch for errors
* @param callback - Callback function when error state changes
* @returns Function to unsubscribe
*/
watchFieldErrors: (
field: keyof T,
callback: (hasError: boolean, errorMessage: string | null) => void
) => {
return stateObserver.subscribe((state) => {
const errors = state.errors;
const fieldErrors: Record<string, any> = {};
// Look for errors in all groups
// Use Object.entries to get type-safe access to the errors object
Object.entries(errors || {}).forEach(([key, errorValue]) => {
// Match pattern like groups.0.data.fieldName
const match = key.match(/groups\.(\d+)\.data\.(.+)/);
if (match && match[2].split('.')[0] === String(field)) {
const groupIndex = parseInt(match[1], 10);
if (!fieldErrors[groupIndex]) {
fieldErrors[groupIndex] = errorValue;
}
}
});
// Call callback with error information
const hasAnyError = Object.keys(fieldErrors).length > 0;
const firstErrorMessage = hasAnyError ?
Object.values(fieldErrors)[0]?.message || 'Invalid field' :
null;
callback(hasAnyError, firstErrorMessage);
});
},
/**
* Create a debounced auto-save function that triggers when form becomes dirty
*
* @param saveFunction - Function to call when auto-save triggers
* @param debounceMs - Debounce delay in milliseconds
* @returns Object with control methods
*/
createAutoSave: (
saveFunction: () => void | Promise<void>,
debounceMs: number = 1000
) => {
let timeoutId: NodeJS.Timeout | null = null;
let isEnabled = true;
// Subscribe to form state changes
const subscription = stateObserver.subscribe((state, prevState) => {
// Only trigger when form becomes or remains dirty
if (!isEnabled || !state.isDirty) return;
// Clear any existing timeout
if (timeoutId) {
clearTimeout(timeoutId);
}
// Set new timeout
timeoutId = setTimeout(() => {
saveFunction();
}, debounceMs);
});
// Return control methods
return {
stop: () => {
isEnabled = false;
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
},
start: () => {
isEnabled = true;
},
trigger: () => {
if (isEnabled) {
saveFunction();
}
},
unsubscribe: subscription.unsubscribe
};
}
}), [fieldWatcher, stateObserver]);
};