UNPKG

envoc-form

Version:

Envoc form components

126 lines (111 loc) 4.67 kB
import { useContext, useEffect } from 'react'; import { useField, useFormikContext } from 'formik'; import { CustomFieldInputProps } from './CustomFieldInputProps'; import { CustomFieldMetaProps } from './CustomFieldMetaProps'; import { FieldNameContext } from './FieldNameContext'; import { ServerErrorContext } from '../Form/ServerErrorContext'; import { NormalizationFunction } from '../Normalization/NormalizationFunction'; import { ValidationFunction } from '../Validation/ValidationFunction'; export interface useStandardFieldProps<TValue> { /** Id of the field. */ id?: string; /** Name of the field. */ name: string; /** Whether the field should be disabled. */ disabled?: boolean; /** Function to validate the field. */ validate?: ValidationFunction<TValue> | ValidationFunction<TValue>[]; /** Function to modify the field value without making the form dirty. (e.g. phone number) */ normalize?: NormalizationFunction<TValue>; } /** Provides a consistent way to deal with all form fields (non array). */ export default function useStandardField<TValue>({ id: providedId, name: providedName, disabled, validate, normalize, }: useStandardFieldProps<TValue>): [ CustomFieldInputProps<TValue>, CustomFieldMetaProps<TValue> ] { // because the formik errors are evaluated all at the same time we need to keep server errors separate const { getError: getServerError, setError: setServerError } = useContext(ServerErrorContext); // ensure that form section values are obeyed, e.g. homeAddress.zipCode const fieldNameContextValue = useContext(FieldNameContext); const name = fieldNameContextValue ? `${fieldNameContextValue}.${providedName}` : providedName; // ensure that nested contexts don't have duplicate id issues when an id is specified const id = providedId ? fieldNameContextValue ? `${fieldNameContextValue}.${providedId}` : providedId : name; // ensure that our custom validation rules are handled // e.g. we allow arrays of validators const [formikInput, formikMeta] = useField<TValue>({ name, id: id ? id : name, disabled: disabled, validate: callAllValidators, }); const { setFieldTouched, setFieldValue, isSubmitting } = useFormikContext(); const touched = formikMeta.touched !== false && formikMeta.touched !== undefined; useEffect(() => { if (!touched && isSubmitting) { // because we do not always register all fields up front. // e.g. formik expects even a 'create' form to have all fields given, at least, blank values // It looks like this was going to be a thing: https://github.com/jaredpalmer/formik/issues/691 // Formik appears to not have an active maintainer: https://github.com/jaredpalmer/formik/discussions/3526 // We previously had a different fix in place using handleBlur, but it was causing an infinite update cycle. // This was noted as existing, but there was a note about it not working for FieldArray (this does appear to work in my testing with FieldArray) setFieldTouched(name); } }, [isSubmitting, name, setFieldTouched, touched]); // these are the props we expect consumers of this hook to pass directly to the input (or other control) const resultInput: CustomFieldInputProps<TValue> = { name: formikInput.name, // pass any direct from server props through normalize without making the form dirty (e.g. phone number) value: normalize ? normalize(formikInput.value) : formikInput.value, onChange: handleChange, onBlur: handleBlur, // extensions to formik id: id, }; const resultMeta: CustomFieldMetaProps<TValue> = { ...formikMeta, error: getServerError(name) || (touched ? formikMeta.error : undefined), // extensions to formik warning: undefined, // TODO - did this never work? touched: touched, }; return [resultInput, resultMeta]; function handleBlur() { formikInput.onBlur({ target: { name: name } }); } function handleChange(value: TValue) { if (disabled) { return; } const normalized = normalize ? normalize(value) : value; setFieldValue(name, normalized); setServerError(name, undefined); } function callAllValidators(value: TValue) { if (disabled || !validate) { return; } if (!Array.isArray(validate)) { return validate(value); } for (let i = 0; i < validate.length; i++) { const result = validate[i](value); if (result) { return result; } } } }