import { InputBaseProps } from '@mui/material'; import { cloneElement, ForwardedRef, forwardRef, ReactElement, useCallback, useEffect, useImperativeHandle, useMemo, useState, } from 'react'; import createInputOnChangeHandler from '../lib/createInputOnChangeHandler'; import { createTestInputFunction } from '../lib/test_input'; import useIsFirstRender from '../hooks/useIsFirstRender'; type InputWithRefOptionalPropsWithDefault< TypeName extends keyof MapToInputType, > = { createInputOnChangeHandlerOptions?: CreateInputOnChangeHandlerOptions; required?: boolean; valueKey?: string; valueType?: TypeName; }; type InputWithRefOptionalPropsWithoutDefault = { inputTestBatch?: InputTestBatch; onFirstRender?: (args: { isRequired: boolean }) => void; }; type InputWithRefOptionalProps = InputWithRefOptionalPropsWithDefault & InputWithRefOptionalPropsWithoutDefault; type InputWithRefProps< TypeName extends keyof MapToInputType, InputComponent extends ReactElement, > = InputWithRefOptionalProps & { input: InputComponent; }; type InputForwardedRefContent = { getIsChangedByUser?: () => boolean; getValue?: () => MapToInputType[TypeName]; isValid?: () => boolean; setValue?: StateSetter; }; const INPUT_TEST_ID = 'input'; const MAP_TO_INITIAL_VALUE: MapToInputType = { number: 0, string: '', }; const INPUT_WITH_REF_DEFAULT_PROPS: Required< InputWithRefOptionalPropsWithDefault<'string'> > & InputWithRefOptionalPropsWithoutDefault = { createInputOnChangeHandlerOptions: {}, required: false, valueKey: 'value', valueType: 'string', }; const InputWithRef = forwardRef( ( { createInputOnChangeHandlerOptions: { postSet: postSetAppend, ...restCreateInputOnChangeHandlerOptions } = INPUT_WITH_REF_DEFAULT_PROPS.createInputOnChangeHandlerOptions as CreateInputOnChangeHandlerOptions, input, inputTestBatch, onFirstRender, required: isRequired = INPUT_WITH_REF_DEFAULT_PROPS.required, valueKey = INPUT_WITH_REF_DEFAULT_PROPS.valueKey, valueType = INPUT_WITH_REF_DEFAULT_PROPS.valueType as TypeName, }: InputWithRefProps, ref: ForwardedRef>, ) => { const { props: { onBlur: initOnBlur, onChange: initOnChange, onFocus: initOnFocus, value: initValue = MAP_TO_INITIAL_VALUE[valueType], ...restInitProps }, } = input; const isFirstRender = useIsFirstRender(); const [inputValue, setInputValue] = useState(initValue); const [isChangedByUser, setIsChangedByUser] = useState(false); const [isInputValid, setIsInputValid] = useState(false); const setValue: StateSetter = useCallback((value) => { setInputValue(value as MapToInputType[TypeName]); }, []); const testInput: TestInputFunction | undefined = useMemo(() => { let result; if (inputTestBatch) { inputTestBatch.isRequired = isRequired; result = createTestInputFunction({ [INPUT_TEST_ID]: inputTestBatch, }); } return result; }, [inputTestBatch, isRequired]); const onBlur = useMemo( () => initOnBlur ?? (testInput && (({ target: { value } }) => { const isValid = testInput({ inputs: { [INPUT_TEST_ID]: { value } }, }); setIsInputValid(isValid); })), [initOnBlur, testInput], ); const onChange = useMemo( () => createInputOnChangeHandler({ postSet: (...args) => { setIsChangedByUser(true); initOnChange?.call(null, ...args); postSetAppend?.call(null, ...args); }, set: setValue, setType: valueType, ...restCreateInputOnChangeHandlerOptions, }), [ initOnChange, postSetAppend, restCreateInputOnChangeHandlerOptions, setValue, valueType, ], ); const onFocus = useMemo( () => initOnFocus ?? (inputTestBatch && (() => { inputTestBatch.defaults?.onSuccess?.call(null, { append: {} }); })), [initOnFocus, inputTestBatch], ); useEffect(() => { if (isFirstRender) { onFirstRender?.call(null, { isRequired }); } }, [isFirstRender, isRequired, onFirstRender]); useImperativeHandle( ref, () => ({ getIsChangedByUser: () => isChangedByUser, getValue: () => inputValue, isValid: () => isInputValid, setValue, }), [inputValue, isChangedByUser, isInputValid, setValue], ); return cloneElement(input, { ...restInitProps, onBlur, onChange, onFocus, required: isRequired, [valueKey]: inputValue, }); }, ); InputWithRef.defaultProps = INPUT_WITH_REF_DEFAULT_PROPS; InputWithRef.displayName = 'InputWithRef'; export type { InputForwardedRefContent, InputWithRefProps }; export default InputWithRef;