envoc-form
Version:
Envoc form components
126 lines (111 loc) • 4.67 kB
text/typescript
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;
}
}
}
}