import ButtonContext from '../silke-button/context'; import { FormFieldProps, FormValidator } from '../silke-form'; import { FieldRegisterModel, FormObjectProvider } from '../silke-form/form-context'; import { useFormField } from '../../hooks/form'; import React, { useCallback, useRef, useState } from 'react'; export type ErrorMap = { [key in keyof T]?: string }; export interface SilkeObjectFieldProps extends FormFieldProps { children?: React.ReactNode; showAllErrors?: boolean; errors?: ErrorMap; onErrors?: (errors: ErrorMap) => void; } const validator: FormValidator> = (props, value) => undefined; export function hasErrors(errors: ErrorMap): boolean { for (const key in errors) { if (errors[key]) return true; } return false; } export function SilkeObjectField>(props: SilkeObjectFieldProps) { const { children, value = {} as T, errors: externalErrors, onErrors, onChange, } = useFormField(props, validator); const fieldRefs = useRef({}); const [hasHiddenErrors, setHasHiddenErrors] = useState(false); const hiddenErrorRefs = useRef>({}); const [errors, setErrors] = useState>({}); // Need to copy errors and value into ref so the un-registerField can clear errors and value related to a field const errorsRef = useRef>(); const valueRef = useRef(value); errorsRef.current = errors; valueRef.current = value || {}; const fieldHasErrors = hasErrors(errors) || hasHiddenErrors; const validateField = (name: keyof T) => { const error = fieldRefs.current[name]?.getError(); delete hiddenErrorRefs.current[name]; const newHasHiddenErrors = hasErrors(hiddenErrorRefs.current); if (hasHiddenErrors !== newHasHiddenErrors) setHasHiddenErrors(newHasHiddenErrors); errorsRef.current = { ...errors, [name]: error }; setErrors(errorsRef.current); if (onErrors) onErrors({ ...hiddenErrorRefs.current, ...errorsRef.current }); }; const registerField = useCallback( (name: keyof T, v: FieldRegisterModel) => { fieldRefs.current[name] = v; const error = v.getError(); if (error) hiddenErrorRefs.current[name] = error; else delete hiddenErrorRefs.current[name]; setHasHiddenErrors(hasErrors(hiddenErrorRefs.current)); if (onErrors) onErrors({ ...hiddenErrorRefs.current, ...errorsRef.current }); return () => { // Remove hidden errors if field disappears const hiddenErrors = hiddenErrorRefs.current as ErrorMap; if (hiddenErrors) delete hiddenErrors[name]; // Remove error if field disappears const newErrors = { ...errorsRef.current } as ErrorMap; delete newErrors[name]; setErrors(newErrors); setHasHiddenErrors(hasErrors(hiddenErrorRefs.current)); if (onErrors) onErrors({ ...hiddenErrorRefs.current, ...errorsRef.current }); // This code deletes values if the form un mounts // it is supposed to delete value if field unmounts, // but react unmounts bottom up so seems impossible to detect if form is unmounted // if (onChange) { // const newValue = { ...valueRef.current }; // delete newValue[name]; // onChange(newValue); // } }; }, [hiddenErrorRefs], ); return ( { const newValue = { ...value, [name]: update, } as T; valueRef.current = newValue; validateField(name); if (onChange) onChange(newValue); }} validate={validateField} register={registerField} > {children} ); }