diff --git a/striker-ui/components/InputWithRef.tsx b/striker-ui/components/InputWithRef.tsx index 06b532b9..9e9995dc 100644 --- a/striker-ui/components/InputWithRef.tsx +++ b/striker-ui/components/InputWithRef.tsx @@ -1,9 +1,12 @@ +import { InputBaseProps } from '@mui/material'; import { cloneElement, ForwardedRef, forwardRef, ReactElement, + useEffect, useImperativeHandle, + useMemo, useState, } from 'react'; @@ -11,16 +14,29 @@ import createInputOnChangeHandler, { CreateInputOnChangeHandlerOptions, MapToStateSetter, } from '../lib/createInputOnChangeHandler'; +import { createTestInputFunction } from '../lib/test_input'; +import useIsFirstRender from '../hooks/useIsFirstRender'; type InputWithRefTypeMap = Pick; -type InputWithRefOptionalProps = { +type InputWithRefOptionalPropsWithDefault< + TypeName extends keyof InputWithRefTypeMap, +> = { createInputOnChangeHandlerOptions?: Omit< CreateInputOnChangeHandlerOptions, 'set' >; + required?: boolean; valueType?: TypeName | 'string'; }; +type InputWithRefOptionalPropsWithoutDefault = { + inputTestBatch?: InputTestBatch; + onFirstRender?: (args: { isRequired: boolean }) => void; +}; + +type InputWithRefOptionalProps = + InputWithRefOptionalPropsWithDefault & + InputWithRefOptionalPropsWithoutDefault; type InputWithRefProps< TypeName extends keyof InputWithRefTypeMap, @@ -32,18 +48,22 @@ type InputWithRefProps< type InputForwardedRefContent = { getIsChangedByUser?: () => boolean; getValue?: () => InputWithRefTypeMap[TypeName]; + isValid?: () => boolean; setValue?: MapToStateSetter[TypeName]; }; +const INPUT_TEST_ID = 'input'; const MAP_TO_INITIAL_VALUE: InputWithRefTypeMap = { number: 0, string: '', }; const INPUT_WITH_REF_DEFAULT_PROPS: Required< - InputWithRefOptionalProps<'string'> -> = { + InputWithRefOptionalPropsWithDefault<'string'> +> & + InputWithRefOptionalPropsWithoutDefault = { createInputOnChangeHandlerOptions: {}, + required: false, valueType: 'string', }; @@ -58,18 +78,67 @@ const InputWithRef = forwardRef( ...restCreateInputOnChangeHandlerOptions } = INPUT_WITH_REF_DEFAULT_PROPS.createInputOnChangeHandlerOptions, input, + inputTestBatch, + onFirstRender, + required: isRequired = INPUT_WITH_REF_DEFAULT_PROPS.required, valueType = INPUT_WITH_REF_DEFAULT_PROPS.valueType, }: InputWithRefProps, ref: ForwardedRef>, ) => { const { - props: { onChange: initOnChange, value: initValue, ...restInitProps }, + props: { + onBlur: initOnBlur, + onChange: initOnChange, + onFocus: initOnFocus, + value: initValue = MAP_TO_INITIAL_VALUE[valueType], + ...restInitProps + }, } = input; - const [value, setValue] = useState( - initValue ?? MAP_TO_INITIAL_VALUE[valueType], + const isFirstRender = useIsFirstRender(); + + const [inputValue, setInputValue] = useState( + initValue, ) as [InputWithRefTypeMap[TypeName], MapToStateSetter[TypeName]]; const [isChangedByUser, setIsChangedByUser] = useState(false); + const [isInputValid, setIsInputValid] = useState(false); + + 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 onFocus = useMemo( + () => + initOnFocus ?? + (inputTestBatch && + (() => { + inputTestBatch.defaults?.onSuccess?.call(null, { append: {} }); + })), + [initOnFocus, inputTestBatch], + ); const onChange = createInputOnChangeHandler({ postSet: (...args) => { @@ -77,22 +146,36 @@ const InputWithRef = forwardRef( initOnChange?.call(null, ...args); postSetAppend?.call(null, ...args); }, - set: setValue, + set: setInputValue, setType: valueType, ...restCreateInputOnChangeHandlerOptions, }); + useEffect(() => { + if (isFirstRender) { + onFirstRender?.call(null, { isRequired }); + } + }, [isFirstRender, isRequired, onFirstRender]); + useImperativeHandle( ref, () => ({ getIsChangedByUser: () => isChangedByUser, - getValue: () => value, - setValue, + getValue: () => inputValue, + isValid: () => isInputValid, + setValue: setInputValue, }), - [isChangedByUser, value], + [inputValue, isChangedByUser, isInputValid], ); - return cloneElement(input, { ...restInitProps, onChange, value }); + return cloneElement(input, { + ...restInitProps, + onBlur, + onChange, + onFocus, + required: isRequired, + value: inputValue, + }); }, );