import { CSSProperties, ElementType, useContext, useMemo, useState, } from 'react'; import classNames from 'classnames'; import { Form as FormikFormWrapper, Formik, FormikHelpers, useFormikContext, FormikProps, } from 'formik'; import FocusError from './FocusError'; import FormBasedPreventNavigation from './FormBasedPreventNavigation'; import { ServerErrorContext, ServerErrorContextProps, ServerErrors, } from './ServerErrorContext'; import Field, { FieldProps } from '../Field/Field'; import FieldArray, { FieldArrayProps } from '../FieldArray/FieldArray'; import { FormDefaults } from '../FormDefaults'; import objectContainsNonSerializableProperty from '../utils/objectContainsNonSerializableProperty'; import objectToFormData from '../utils/objectToFormData'; import { ValidatedApiResult } from '../Validation/ValidatedApiResult'; import { ValidationError } from '../Validation/ValidationError'; // This exposes the builder that ensures only "name" values on the given TForm can be used // Further, each Field can then infer the proper type given the name export type FormBuilderProp = { // the first set of <> is a generic method signature - we don't split this off because we need the TForm closure Field: ( props: FieldProps ) => JSX.Element; // assumes this is never null - thought he final component may not render FieldArray: ( props: FieldArrayProps ) => JSX.Element; }; export interface FullFormProps { /** The `` and `` components. */ children: (formBuilder: FormBuilderProp) => JSX.Element; /** Submission handler */ onSubmit: ( formValues: TForm, formikBag: FormikHelpers ) => Promise; /** Submission handler for forms that use [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData).*/ onFormDataSubmit: ( formValues: FormData, formikBag: FormikHelpers ) => Promise; className?: string; style?: CSSProperties; /** Prevent the user from leaving the form if they have edited any field. This is presented as a JS `alert()`. */ ignoreLostChanges?: boolean; /** The intitial values of the form. */ initialValues?: TForm; /** A ref to the form element. */ innerRef?: React.Ref>; } type RequireAtLeastOne = Pick< T, Exclude > & { [K in Keys]-?: Required> & Partial>>; }[Keys]; export type FormProps = RequireAtLeastOne< FullFormProps, 'onSubmit' | 'onFormDataSubmit' >; /** Define a form. Uses [formik](https://formik.org/docs/overview). Usually contains many `` components. */ export default function Form({ children, className, style, ignoreLostChanges, onSubmit, onFormDataSubmit, initialValues, ...props }: FormProps) { // formik resets all error on each blur (with our settings) // this means that ALL errors from the server disappear when any one field is blurred // So, we have to store server errors ourselves // Since we only use useStandardFormInput, that means there is only one consumer const [serverErrors, setServerErrors] = useState({}); const serverErrorContextValue = useMemo( () => ({ errors: serverErrors, getError: (path) => { const lowered = path.toLowerCase(); return serverErrors && serverErrors[lowered]; }, setError: (path, errorMessage) => { const lowered = path.toLowerCase(); setServerErrors( Object.assign({}, serverErrors, { [lowered]: !errorMessage ? undefined : errorMessage, }) ); }, }), [serverErrors] ); return ( {children({ // hack for ref forwarding Field: Field as any, FieldArray: FieldArray as any, })} ); function handleSubmit(values: TForm, formikBag: FormikHelpers) { let formData: FormData | undefined = undefined; let submitFunc: () => Promise; if (objectContainsNonSerializableProperty(values)) { formData = objectToFormData(values, { indices: true, dotNotation: true, allowEmptyArrays: true, noFileListBrackets: true, }); if (onFormDataSubmit === undefined) { throw new Error( 'No onFormDataSubmit supplied for non-serializable properties.' ); } submitFunc = () => onFormDataSubmit(formData ?? new FormData(), formikBag); } else { if (onSubmit === undefined) { formData = objectToFormData(values, { indices: true, dotNotation: true, allowEmptyArrays: true, noFileListBrackets: true, }); if (onFormDataSubmit === undefined) { // This error should never occur, as this case is covered by RequireAtLeastOne type safety throw new Error( 'No onFormDataSubmit supplied for non-serializable properties.' ); } submitFunc = () => onFormDataSubmit(formData ?? new FormData(), formikBag); } else { submitFunc = () => onSubmit(values, formikBag); } } return submitFunc() .then((response) => { return response; }) .catch((err) => { //this is an http error if ( err && err.response && err.response.data && err.response.data.validationFailures ) { try { const serverErrors = err.response.data.validationFailures.reduce( (acc: ServerErrors, value: ValidationError) => { // for simplicity, just keep it to one server error at a time per path // don't care of the property name case changes const path = value.propertyName?.toLowerCase(); if (!!path && !!value.errorMessage) { acc[path] = value.errorMessage; } return acc; }, {} as ServerErrors ); setServerErrors(serverErrors); } catch (err) { console.error('Failure to getErrorObject'); console.error(err); throw err; } } throw err; }); } } Form.DisplayFormState = DisplayFormState; function DisplayFormState() { const formState = useFormikContext(); const serverErrorContext = useContext(ServerErrorContext); return (
        {serverErrorContext && serverErrorContext.errors && (
          
serverErrors = {JSON.stringify(serverErrorContext.errors, null, 2)}
)} formState = {JSON.stringify(formState, null, 2)}
); }