import React from 'react'; import { useState, useMemo } from 'react'; import { FieldPath, FormGroup } from '../../form/form-groups'; import { ErrorRenderer, ErrorRendererContext, ErrorRendererProps, ErrorRendererReturn, FieldErrorRendererConfig, } from './types'; import { useMemoizedCallback } from '../../../common/useMemoizedCallback'; /** * Default error renderer that returns a simple error message */ const defaultErrorRenderer = >({ error, }: ErrorRendererProps): React.ReactNode => { const errorMessage = Array.isArray(error) ? error[0] : error; return
{errorMessage}
; }; /** * Hook for managing custom error renderers in Reform forms * * @template T - The type of form data * @param groups - The form groups * @returns Object with error renderer utilities * * @example * // Basic usage * const reform = useReform(config); * const { registerErrorRenderer, renderError } = useErrorRenderer(reform.getGroups()); * * // Register a custom renderer for email fields * useEffect(() => { * const unregister = registerErrorRenderer({ * field: 'email', * renderer: ({ error }) => * }); * * return unregister; * }, []); * * // Use the renderer in your form * return ( *
* * {renderError('email', reform.getFieldError(0, 'email'), 0)} *
* ); */ export const useErrorRenderer = >( groups: FormGroup[] ): ErrorRendererReturn => { // State for the error renderer context const [context, setContext] = useState>({ defaultRenderer: defaultErrorRenderer, fieldRenderers: [], showAllErrors: false, contextData: {}, }); /** * Register a field-specific error renderer */ const registerErrorRenderer = useMemoizedCallback( (config: FieldErrorRendererConfig) => { const { field, renderer, priority = 0 } = config; // Add the new renderer to the list setContext(prev => { const newRenderers = [ ...prev.fieldRenderers, { field, renderer, priority }, ]; // Sort by priority (higher numbers first) newRenderers.sort((a, b) => (b.priority || 0) - (a.priority || 0)); return { ...prev, fieldRenderers: newRenderers, }; }); // Return a function to unregister the renderer return () => { setContext(prev => ({ ...prev, fieldRenderers: prev.fieldRenderers.filter(r => r !== config), })); }; }, [] ); /** * Set the default error renderer */ const setDefaultRenderer = useMemoizedCallback( (renderer: ErrorRenderer) => { setContext(prev => ({ ...prev, defaultRenderer: renderer, })); }, [] ); /** * Check if a field matches a field pattern */ const fieldMatches = useMemoizedCallback( ( fieldPattern: FieldPath | RegExp | ((field: FieldPath) => boolean), field: FieldPath ): boolean => { if (typeof fieldPattern === 'function') { return fieldPattern(field); } if (fieldPattern instanceof RegExp) { return fieldPattern.test(String(field)); } return fieldPattern === field; }, [] ); /** * Get the appropriate renderer for a field */ const getRendererForField = useMemoizedCallback( (field: FieldPath): ErrorRenderer => { // Find the first matching renderer const matchingConfig = context.fieldRenderers.find(config => fieldMatches(config.field, field) ); return matchingConfig?.renderer || context.defaultRenderer; }, [context.fieldRenderers, context.defaultRenderer, fieldMatches] ); /** * Render an error for a specific field */ const renderError = useMemoizedCallback( ( field: FieldPath, error: string | string[] | undefined, groupIndex: number ): React.ReactNode => { if (!error) return null; const renderer = getRendererForField(field); const value = groupIndex >= 0 && groupIndex < groups.length ? groups[groupIndex].data[field as keyof T] : undefined; const props: ErrorRendererProps = { field, error, value, groupIndex, groups, meta: context.contextData, }; return renderer(props); }, [groups, context.contextData, getRendererForField] ); /** * Set whether to show all errors or just the first one */ const setShowAllErrors = useMemoizedCallback((showAll: boolean) => { setContext(prev => ({ ...prev, showAllErrors: showAll, })); }, []); /** * Update the context data */ const updateContextData = useMemoizedCallback((data: Record) => { setContext(prev => ({ ...prev, contextData: { ...prev.contextData, ...data, }, })); }, []); // Return memoized object to prevent unnecessary re-renders return useMemo( () => ({ registerErrorRenderer, setDefaultRenderer, renderError, getRendererForField, setShowAllErrors, updateContextData, context, }), [ registerErrorRenderer, setDefaultRenderer, renderError, getRendererForField, setShowAllErrors, updateContextData, context, ] ); };