import { ForwardedRef, ReactElement, cloneElement, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState, } from 'react'; import INPUT_TYPES from '../lib/consts/INPUT_TYPES'; import MAP_TO_VALUE_CONVERTER from '../lib/consts/MAP_TO_VALUE_CONVERTER'; const UncontrolledInput = forwardRef( ( props: UncontrolledInputProps, ref: ForwardedRef>, ) => { const { input, onChange = ({ handlers: { base, origin } }, ...args) => { base?.call(null, ...args); origin?.call(null, ...args); }, onMount, onUnmount, } = props; const { props: inputProps } = input; const { valueKey, valueType } = useMemo(() => { const { type } = inputProps; let vkey: 'checked' | 'value' = 'value'; let vtype: keyof MapToInputType = 'string'; if (type === INPUT_TYPES.checkbox) { vkey = 'checked'; vtype = 'boolean'; } return { valueKey: vkey, valueType: vtype, }; }, [inputProps]); const { onChange: inputOnChange, [valueKey]: inputValue, ...restInputProps } = inputProps; const [value, setValue] = useState(inputValue); const baseChangeEventHandler = useCallback< React.ChangeEventHandler >( ({ target: { [valueKey]: changed } }) => { const converted = MAP_TO_VALUE_CONVERTER[valueType]( changed, ) as MapToInputType[ValueType]; setValue(converted); }, [valueKey, valueType], ); const changeEventHandler = useCallback< React.ChangeEventHandler >( (...args) => onChange?.call( null, { handlers: { base: baseChangeEventHandler, origin: inputOnChange } }, ...args, ), [baseChangeEventHandler, inputOnChange, onChange], ); // Handle mount/unmount events; these only happen once hence no deps useEffect(() => { onMount?.call(null); return onUnmount; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useImperativeHandle( ref, () => ({ get: () => value, set: setValue, }), [value], ); return cloneElement(input, { ...restInputProps, onChange: changeEventHandler, [valueKey]: value, }); }, ); UncontrolledInput.displayName = 'UncontrolledInput'; export default UncontrolledInput;