-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
/
useInput.ts
161 lines (145 loc) · 5.64 KB
/
useInput.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import { ReactElement, useEffect } from 'react';
import {
ControllerFieldState,
ControllerRenderProps,
useController,
UseControllerProps,
UseControllerReturn,
UseFormStateReturn,
} from 'react-hook-form';
import get from 'lodash/get';
import { useRecordContext } from '../controller';
import { composeValidators, Validator } from './validate';
import isRequired from './isRequired';
import { useFormGroupContext } from './useFormGroupContext';
import { useFormGroups } from './useFormGroups';
import { useApplyInputDefaultValues } from './useApplyInputDefaultValues';
import { useEvent } from '../util';
// replace null or undefined values by empty string to avoid controlled/uncontrolled input warning
const defaultFormat = (value: any) => (value == null ? '' : value);
// parse empty string into null as it's more suitable for a majority of backends
const defaultParse = (value: string) => (value === '' ? null : value);
export const useInput = <ValueType = any>(
props: InputProps<ValueType>
): UseInputValue => {
const {
defaultValue,
format = defaultFormat,
id,
isRequired: isRequiredOption,
name,
onBlur: initialOnBlur,
onChange: initialOnChange,
parse = defaultParse,
source,
validate,
...options
} = props;
const finalName = name || source;
const formGroupName = useFormGroupContext();
const formGroups = useFormGroups();
const record = useRecordContext();
useEffect(() => {
if (!formGroups || formGroupName == null) {
return;
}
formGroups.registerField(source, formGroupName);
return () => {
formGroups.unregisterField(source, formGroupName);
};
}, [formGroups, formGroupName, source]);
const sanitizedValidate = Array.isArray(validate)
? composeValidators(validate)
: validate;
// Fetch the defaultValue from the record if available or apply the provided defaultValue.
// This ensures dynamically added inputs have their value set correctly (ArrayInput for example).
// We don't do this for the form level defaultValues so that it works as it should in react-hook-form
// (i.e. field level defaultValue override form level defaultValues for this field).
const { field: controllerField, fieldState, formState } = useController({
name: finalName,
defaultValue: get(record, source, defaultValue),
rules: {
validate: async (value, values) => {
if (!sanitizedValidate) return true;
const error = await sanitizedValidate(value, values, props);
if (!error) return true;
// react-hook-form expects errors to be plain strings but our validators can return objects
// that have message and args.
// To avoid double translation for users that validate with a schema instead of our validators
// we use a special format for our validators errors.
// The ValidationError component will check for this format and extract the message and args
// to translate.
return `@@react-admin@@${JSON.stringify(error)}`;
},
},
...options,
// Workaround for https://github.com/react-hook-form/react-hook-form/issues/10907
// FIXME - remove when fixed
// @ts-ignore - only exists since react-hook-form 7.46.0
disabled: options.disabled || undefined,
});
// Because our forms may receive an asynchronously loaded record for instance,
// they may reset their default values which would override the input default value.
// This hook ensures that the input default value is applied when a new record is loaded but has
// no value for the input.
useApplyInputDefaultValues({ inputProps: props });
const onBlur = useEvent((...event: any[]) => {
controllerField.onBlur();
if (initialOnBlur) {
initialOnBlur(...event);
}
});
const onChange = useEvent((...event: any[]) => {
const eventOrValue = (props.type === 'checkbox' &&
event[0]?.target?.value === 'on'
? event[0].target.checked
: event[0]?.target?.value ?? event[0]) as any;
controllerField.onChange(parse ? parse(eventOrValue) : eventOrValue);
if (initialOnChange) {
initialOnChange(...event);
}
});
const field = {
...controllerField,
value: format ? format(controllerField.value) : controllerField.value,
onBlur,
onChange,
};
return {
id: id || source,
field,
fieldState,
formState,
isRequired: isRequiredOption || isRequired(validate),
};
};
export type InputProps<ValueType = any> = Omit<
UseControllerProps,
'name' | 'defaultValue' | 'rules'
> &
Partial<UseControllerReturn> & {
alwaysOn?: any;
defaultValue?: any;
format?: (value: ValueType) => any;
id?: string;
isRequired?: boolean;
label?: string | ReactElement | false;
helperText?: string | ReactElement | false;
name?: string;
onBlur?: (...event: any[]) => void;
onChange?: (...event: any[]) => void;
parse?: (value: any) => ValueType;
type?: string;
resource?: string;
source: string;
validate?: Validator | Validator[];
readOnly?: boolean;
disabled?: boolean;
};
export type UseInputValue = {
id: string;
isRequired: boolean;
field: ControllerRenderProps;
formState: UseFormStateReturn<Record<string, string>>;
fieldState: ControllerFieldState;
};