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.
110 lines
2.6 KiB
110 lines
2.6 KiB
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( |
|
<ValueType extends keyof MapToInputType, InputElement extends ReactElement>( |
|
props: UncontrolledInputProps<InputElement>, |
|
ref: ForwardedRef<UncontrolledInputForwardedRefContent<ValueType>>, |
|
) => { |
|
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<MapToInputType[ValueType]>(inputValue); |
|
|
|
const baseChangeEventHandler = useCallback< |
|
React.ChangeEventHandler<HTMLInputElement> |
|
>( |
|
({ target: { [valueKey]: changed } }) => { |
|
const converted = MAP_TO_VALUE_CONVERTER[valueType]( |
|
changed, |
|
) as MapToInputType[ValueType]; |
|
|
|
setValue(converted); |
|
}, |
|
[valueKey, valueType], |
|
); |
|
|
|
const changeEventHandler = useCallback< |
|
React.ChangeEventHandler<HTMLInputElement> |
|
>( |
|
(...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;
|
|
|