import type { ReactNode } from 'react' import React, { useMemo, useRef } from 'react' import type { FormProps as FinalFormProps } from 'react-final-form' import { Form as FinalForm } from 'react-final-form' import type { FormApi, SubmissionErrors, AnyObject } from 'final-form' import { getIn, setIn } from 'final-form' import { useNotifications } from '@toptal/picasso-notification' import { createScrollToErrorDecorator } from '../utils' import type { Validators, FormContextProps } from './FormContext' import { FormContext, createFormContext } from './FormContext' import type { Props as FormProps } from './FormRenderer' import FormRenderer from './FormRenderer' import { setActiveFieldTouched, setHasMultilineCounter } from './mutators' export type Props = FinalFormProps & { disableScrollOnError?: boolean autoComplete?: HTMLFormElement['autocomplete'] successSubmitMessage?: ReactNode failedSubmitMessage?: ReactNode scrollOffsetTop?: number layout?: 'horizontal' | 'vertical' labelWidth?: FormProps['labelWidth'] className?: string 'data-testid'?: string } const getValidationErrors = ( validators: Validators, formValues: any, form: FormApi ): SubmissionErrors | void => { let errors: SubmissionErrors Object.entries(validators).forEach(([key, validator]) => { const fieldValue = getIn(formValues, key) const fieldMetaState = form.getFieldState(key) if (!validator) { return } const error = validator(fieldValue, formValues, fieldMetaState) if (error) { errors = setIn(errors || {}, key, error) } }) return errors } export const Form = (props: Props) => { const { autoComplete, children, disableScrollOnError, onSubmit, successSubmitMessage, failedSubmitMessage, decorators = [], mutators = {}, validateOnBlur, 'data-testid': dataTestId, layout, labelWidth, className, ...rest } = props const { showSuccess, showError } = useNotifications() const scrollToErrorDecorator = useMemo( () => createScrollToErrorDecorator({ disableScrollOnError, }), [disableScrollOnError] ) const validationObject = useRef(createFormContext()) const showSuccessNotification = () => { if (!successSubmitMessage) { return } showSuccess(successSubmitMessage) } const showErrorNotification = (errors: SubmissionErrors) => { if (typeof errors === 'string') { showError(errors, undefined, { persist: true }) return } if (!failedSubmitMessage) { return } showError(failedSubmitMessage, undefined, { persist: true }) } const handleSubmit = async ( values: T, form: FormApi, callback?: (errors?: SubmissionErrors) => void ) => { const validationErrors = getValidationErrors( validationObject.current.getValidators(), values, form ) if (validationErrors) { return validationErrors } const submissionErrors = await onSubmit(values, form, callback) if (!submissionErrors) { showSuccessNotification() } else { showErrorNotification(submissionErrors) } return submissionErrors } return ( ( {children} )} onSubmit={handleSubmit} decorators={[...decorators, scrollToErrorDecorator]} mutators={{ ...mutators, setActiveFieldTouched, setHasMultilineCounter, }} validateOnBlur={validateOnBlur} {...rest} /> ) } Form.displayName = 'Form' export default Form