import type { FieldProps, FormikContext } from 'formik'; import { FastField, Field, FormikConsumer } from 'formik'; import { isString, toPath } from 'lodash'; import React from 'react'; import { WatchValue } from '../../WatchValue'; import { useMountStatusRef } from '../../hooks/useMountStatusRef.hook'; import type { IFormInputValidation } from '../inputs'; import type { ICommonFormFieldProps } from './interface'; import type { ILayoutProps } from '../layouts'; import { LayoutContext } from '../layouts'; import { renderContent } from './renderContent'; import { FormikSpelContext, SimpleSpelInput, SpelAwareInputMode, SpelService, SpelToggle } from '../../spel'; import { firstDefined } from '../../../utils'; import type { IValidator } from '../validation'; import { composeValidators, useValidationData, Validators } from '../validation'; export interface IFormikFieldProps { /** * The name/path to the field in the Formik form. * Accepts lodash paths; see: https://lodash.com/docs/#get */ name: string; /** * Toggles between `Field` (false) and `FastField` (true) * Defaults to `FastField` (true) * * Use `fastField={true}` if the field doesn't depend on any other fields or external (i.e., async) data * See: https://jaredpalmer.com/formik/docs/api/fastfield#when-to-use-fastfield */ fastField?: boolean; /** A callback that is invoked whenever the field value changes */ onChange?: (value: T, prevValue: T) => void; /** Configures SpEL-awareness of the input. * Overrides configuration set on FormikSpelContext. */ spelAware?: boolean; } export type IFormikFormFieldProps = ICommonFormFieldProps & IFormikFieldProps; type IFormikFormFieldImplProps = IFormikFormFieldProps & { formik: FormikContext }; const { useCallback, useContext, useState } = React; const revalidateMap = new Map(); // If many formfields request a revalidate at the same time, coalesce the requests and revalidate once on the next tick function coalescedRevalidate(formik: FormikContext) { if (!revalidateMap.has(formik)) { revalidateMap.set(formik, true); setTimeout(() => { revalidateMap.delete(formik); return formik.validateForm(); }); } } /** * Deeply get a value from an object via its path. */ function getIn(obj: any, key: string, defaultValue: any = undefined) { let p = 0; const path = toPath(key); while (obj && p < path.length) { obj = typeof obj == 'string' ? undefined : obj[path[p++]]; } return obj === undefined ? defaultValue : obj; } function FormikFormFieldImpl(props: IFormikFormFieldImplProps) { const { formik } = props; const { name, onChange, fastField: fastFieldProp } = props; const { input, layout, label, help, required, actions, validate, validationMessage, touched: touchedProp } = props; const FieldLayoutFromContext = useContext(LayoutContext); const SpelAwareFromContext = useContext(FormikSpelContext); const formikTouched = getIn(formik.touched, name); const formikError = getIn(formik.errors, props.name); const fastField = firstDefined(fastFieldProp, false); const touched = firstDefined(touchedProp, formikTouched as boolean); const message = firstDefined(validationMessage, formikError as string); const { hidden, category, messageNode } = useValidationData(message, touched); const [internalValidators, setInternalValidators] = useState([]); const addValidator = useCallback((v: IValidator) => setInternalValidators((list) => list.concat(v)), []); const removeValidator = useCallback( (v: IValidator) => setInternalValidators((list) => list.filter((x) => x !== v)), [], ); const revalidate = () => coalescedRevalidate(formik); const mountStatus = useMountStatusRef(); React.useEffect(() => { if (mountStatus.current === 'FIRST_RENDER' && internalValidators.length === 0) { return; } revalidate(); }, [internalValidators]); const validation: IFormInputValidation = { touched, revalidate, addValidator, removeValidator, hidden, category, messageNode, }; const freeformInputAllowed = firstDefined(props.spelAware, SpelAwareFromContext, false); const initialInputMode = React.useMemo(() => { const fieldValue = getIn(props.formik.values, name, ''); return freeformInputAllowed && SpelService.includesSpel(fieldValue) ? SpelAwareInputMode.FREEFORM : SpelAwareInputMode.DEFAULT; }, []); const [inputMode, setInputMode] = React.useState(initialInputMode); const onSpelToggleClick = () => { formik.setFieldValue(name, null); setInputMode(inputMode === SpelAwareInputMode.DEFAULT ? SpelAwareInputMode.FREEFORM : SpelAwareInputMode.DEFAULT); }; const renderField = ({ field }: FieldProps) => { const inputOrSpel = inputMode === SpelAwareInputMode.DEFAULT ? input : SimpleSpelInput; const layoutFromContext = (fieldLayoutProps: ILayoutProps) => ; const layoutRenderPropOrNode = firstDefined(layout, layoutFromContext); const inputElement = renderContent(inputOrSpel, { ...field, validation }); const composedActions = ( <> {actions} {freeformInputAllowed && } ); const layoutProps: ILayoutProps = { label, help, required, actions: composedActions, input: inputElement, validation, }; return ( {renderContent(layoutRenderPropOrNode, layoutProps)} ); }; const validator = createFieldValidator(label, required, [].concat(validate).concat(internalValidators)); if (fastField) { return ; } return ; } /** Returns a Validator composed of all the `validate` functions (and `isRequired` if `required` is truthy) */ export function createFieldValidator( label: IFormikFormFieldProps['label'], required: boolean, validate: IValidator[], ): IValidator { const validators = [!!required && Validators.isRequired()].concat(validate); const validator = composeValidators(validators); if (!validator) { return null; } const labelString = isString(label) ? label : undefined; return (value: any) => validator(value, labelString); } export function FormikFormField(props: IFormikFormFieldProps) { return {(formik) => }; }