Local modifications to ClusterLabs/Anvil by Alteeve
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

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;