You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
7.4 KiB
261 lines
7.4 KiB
import { InputBaseProps } from '@mui/material'; |
|
import { debounce } from 'lodash'; |
|
import { |
|
cloneElement, |
|
ForwardedRef, |
|
forwardRef, |
|
ReactElement, |
|
useCallback, |
|
useEffect, |
|
useImperativeHandle, |
|
useMemo, |
|
useState, |
|
} from 'react'; |
|
|
|
import createInputOnChangeHandler from '../lib/createInputOnChangeHandler'; |
|
import { createTestInputFunction } from '../lib/test_input'; |
|
|
|
type InputWithRefOptionalPropsWithDefault< |
|
TypeName extends keyof MapToInputType, |
|
> = { |
|
createInputOnChangeHandlerOptions?: CreateInputOnChangeHandlerOptions<TypeName>; |
|
required?: boolean; |
|
valueType?: TypeName; |
|
}; |
|
type InputWithRefOptionalPropsWithoutDefault< |
|
TypeName extends keyof MapToInputType, |
|
> = { |
|
debounceWait?: number; |
|
inputTestBatch?: InputTestBatch; |
|
onBlurAppend?: InputBaseProps['onBlur']; |
|
onFirstRender?: InputFirstRenderFunction; |
|
onFocusAppend?: InputBaseProps['onFocus']; |
|
onUnmount?: () => void; |
|
valueKey?: CreateInputOnChangeHandlerOptions<TypeName>['valueKey']; |
|
}; |
|
|
|
type InputWithRefOptionalProps<TypeName extends keyof MapToInputType> = |
|
InputWithRefOptionalPropsWithDefault<TypeName> & |
|
InputWithRefOptionalPropsWithoutDefault<TypeName>; |
|
|
|
type InputWithRefProps< |
|
TypeName extends keyof MapToInputType, |
|
InputComponent extends ReactElement, |
|
> = InputWithRefOptionalProps<TypeName> & { |
|
input: InputComponent; |
|
}; |
|
|
|
type InputForwardedRefContent<TypeName extends keyof MapToInputType> = { |
|
getIsChangedByUser?: () => boolean; |
|
getValue?: () => MapToInputType[TypeName]; |
|
isValid?: () => boolean; |
|
setValue?: StateSetter; |
|
}; |
|
|
|
const INPUT_TEST_ID = 'input'; |
|
const MAP_TO_INITIAL_VALUE: MapToInputType = { |
|
boolean: false, |
|
number: 0, |
|
string: '', |
|
}; |
|
|
|
const INPUT_WITH_REF_DEFAULT_PROPS: Required< |
|
InputWithRefOptionalPropsWithDefault<'string'> |
|
> & |
|
InputWithRefOptionalPropsWithoutDefault<'string'> = { |
|
createInputOnChangeHandlerOptions: {}, |
|
debounceWait: 500, |
|
required: false, |
|
valueType: 'string', |
|
}; |
|
|
|
const InputWithRef = forwardRef( |
|
<TypeName extends keyof MapToInputType, InputComponent extends ReactElement>( |
|
{ |
|
debounceWait = INPUT_WITH_REF_DEFAULT_PROPS.debounceWait, |
|
input, |
|
inputTestBatch, |
|
onBlurAppend, |
|
onFirstRender, |
|
onFocusAppend, |
|
onUnmount, |
|
required: isRequired = INPUT_WITH_REF_DEFAULT_PROPS.required, |
|
valueKey, |
|
valueType = INPUT_WITH_REF_DEFAULT_PROPS.valueType as TypeName, |
|
// Props with initial value that depend on others. |
|
createInputOnChangeHandlerOptions: { |
|
postSet: postSetAppend, |
|
valueKey: onChangeValueKey = valueKey, |
|
...restCreateInputOnChangeHandlerOptions |
|
} = INPUT_WITH_REF_DEFAULT_PROPS.createInputOnChangeHandlerOptions as CreateInputOnChangeHandlerOptions<TypeName>, |
|
}: InputWithRefProps<TypeName, InputComponent>, |
|
ref: ForwardedRef<InputForwardedRefContent<TypeName>>, |
|
) => { |
|
const { props: inputProps } = input; |
|
|
|
const vKey = useMemo( |
|
() => onChangeValueKey ?? ('checked' in inputProps ? 'checked' : 'value'), |
|
[inputProps, onChangeValueKey], |
|
); |
|
|
|
const { |
|
onBlur: initOnBlur, |
|
onChange: initOnChange, |
|
onFocus: initOnFocus, |
|
[vKey]: initValue = MAP_TO_INITIAL_VALUE[valueType], |
|
...restInitProps |
|
} = inputProps; |
|
|
|
const [inputValue, setInputValue] = |
|
useState<MapToInputType[TypeName]>(initValue); |
|
const [isChangedByUser, setIsChangedByUser] = useState<boolean>(false); |
|
const [isInputValid, setIsInputValid] = useState<boolean>(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 doTestAndSet = useCallback( |
|
(value: MapToInputType[TypeName]) => { |
|
const valid = |
|
testInput?.call(null, { |
|
inputs: { [INPUT_TEST_ID]: { value } }, |
|
isIgnoreOnCallbacks: true, |
|
}) ?? false; |
|
|
|
onFirstRender?.call(null, { isValid: valid }); |
|
setIsInputValid(valid); |
|
}, |
|
[onFirstRender, testInput], |
|
); |
|
|
|
const debounceDoTestAndSet = useMemo( |
|
() => debounce(doTestAndSet, debounceWait), |
|
[debounceWait, doTestAndSet], |
|
); |
|
|
|
const onBlur = useMemo<InputBaseProps['onBlur']>( |
|
() => |
|
initOnBlur ?? |
|
(testInput && |
|
((...args) => { |
|
const { |
|
0: { |
|
target: { value }, |
|
}, |
|
} = args; |
|
const isValid = testInput({ |
|
inputs: { [INPUT_TEST_ID]: { value } }, |
|
}); |
|
|
|
setIsInputValid(isValid); |
|
onBlurAppend?.call(null, ...args); |
|
})), |
|
[initOnBlur, onBlurAppend, testInput], |
|
); |
|
const onChange = useMemo( |
|
() => |
|
createInputOnChangeHandler<TypeName>({ |
|
postSet: (...args) => { |
|
setIsChangedByUser(true); |
|
initOnChange?.call(null, ...args); |
|
postSetAppend?.call(null, ...args); |
|
}, |
|
set: (value) => { |
|
setValue(value); |
|
debounceDoTestAndSet(value as MapToInputType[TypeName]); |
|
}, |
|
setType: valueType, |
|
valueKey: vKey, |
|
...restCreateInputOnChangeHandlerOptions, |
|
}), |
|
[ |
|
debounceDoTestAndSet, |
|
initOnChange, |
|
postSetAppend, |
|
restCreateInputOnChangeHandlerOptions, |
|
setValue, |
|
vKey, |
|
valueType, |
|
], |
|
); |
|
const onFocus = useMemo<InputBaseProps['onFocus']>( |
|
() => |
|
initOnFocus ?? |
|
(inputTestBatch && |
|
((...args) => { |
|
inputTestBatch.defaults?.onSuccess?.call(null, { append: {} }); |
|
onFocusAppend?.call(null, ...args); |
|
})), |
|
[initOnFocus, inputTestBatch, onFocusAppend], |
|
); |
|
|
|
/** |
|
* Using any setState function synchronously in the render function |
|
* directly will trigger the 'cannot update a component while readering a |
|
* different component' warning. This can be solved by wrapping the |
|
* setState call(s) in a useEffect hook because it executes **after** the |
|
* render function completes. |
|
*/ |
|
useEffect(() => { |
|
doTestAndSet(inputValue); |
|
|
|
return onUnmount; |
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps |
|
}, []); |
|
|
|
/** |
|
* Update the input value to the init value until it's changed by the user. |
|
* This allows us to populate the input based on value from other field(s). |
|
*/ |
|
useEffect(() => { |
|
if (isChangedByUser || inputValue === initValue || !initValue) return; |
|
|
|
doTestAndSet(initValue); |
|
|
|
setInputValue(initValue); |
|
}, [doTestAndSet, initValue, inputValue, isChangedByUser]); |
|
|
|
useImperativeHandle( |
|
ref, |
|
() => ({ |
|
getIsChangedByUser: () => isChangedByUser, |
|
getValue: () => inputValue, |
|
isValid: () => isInputValid, |
|
setValue, |
|
}), |
|
[inputValue, isChangedByUser, isInputValid, setValue], |
|
); |
|
|
|
return cloneElement(input, { |
|
...restInitProps, |
|
onBlur, |
|
onChange, |
|
onFocus, |
|
required: isRequired, |
|
[vKey]: inputValue, |
|
}); |
|
}, |
|
); |
|
|
|
InputWithRef.defaultProps = INPUT_WITH_REF_DEFAULT_PROPS; |
|
InputWithRef.displayName = 'InputWithRef'; |
|
|
|
export type { InputForwardedRefContent, InputWithRefProps }; |
|
|
|
export default InputWithRef;
|
|
|