React Form Hooks

背景

antdForm表单是使用高阶组件实现的, 而且默认的校验方式需要写在Form.Item上或者通过getFieldDecorator高阶组件生成, 对于函数式组件来说实现非常不友好, 而且也不是集中管理, 分散在各个jsx组件中, 不便于维护查看等, 而且函数式组件支持方式在不确定发布日期的antdv4版本才有计划升级, 所以需要自己实现一套适合于Hooks的表单验证组件

开发

useForm

既然是实现Hooks, 参考state的实现, 我们需要定义form的初始值, 以及相关的设置方法, 同时有个特殊点的是form的字段验证方法, 返回值可以分开返回, 但尽量与antd-form需要的字段一致, 如helper/validateStatus, 而且用Hooks有个好处, 我们可以集中验证字段类型, 不需要分散到各组件中验证了

同时, 可以将常用的Input方法也直接由Hooks返回, 如onChange/setForm/getFieldStatus等, 需要注意的是为了支持多种输入组件, onChange需要处理好对EventValue的泛型支持, 并提供修改的入口

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
type FormValueType = string | number | boolean | any;

const useForm = (
initState: {
[key: string]: FormValueType;
},
rules: { [key in keyof typeof initState]: () => RuleResult } = {},
) => {
const [form, setForm] = useState(initState);
const { formFieldInfoState, getFieldStatus, setInit } = useFormValidator(form, rules);
const setInitForm = (_form: typeof initState) => {
setForm(_form);
setInit();
};

const changeHandlerBuild = <T = React.SyntheticEvent>(
key: keyof typeof initState,
getValue = (event: T) => (event as any).target.value,
changeCheckStatus = true,
) => {
return (event: T) => {
setForm({
..._.set(form, key, getValue(event)),
});
if (changeCheckStatus) {
getFieldStatus(key);
}
};
};
const formInputHandler = <T = string, E = React.SyntheticEvent>(
key: keyof typeof initState,
getEventValue?: (event: E) => T,
defaultValue?: ((value: T) => T) | T,
changeCheckStatus?: boolean,
) => {
return {
// eslint-disable-next-line no-nested-ternary
value: (defaultValue
? _.isFunction(defaultValue)
? defaultValue(_.get(form, key, '') as any)
: defaultValue
: (_.toString(_.get(form, key, '')) as any)) as T,
onChange: changeHandlerBuild(key, getEventValue, changeCheckStatus),
onBlur: () => getFieldStatus(key),
};
};
return [
{ form, formFieldInfoState },
{ setForm, setInitForm, getFieldStatus, formInputHandler, changeHandlerBuild },
];
};

export default useForm;

useFormValidator

上面的useForm已经把formvalue/change等事件处理完了, 那么接下来再实现一个单独验证表单数据的Hooks就能实现与antd的表单同样的效果

同时希望验证的语法不要太复杂, 能直接支持原始函数并且支持返回错误类型即可, 类似parameterrule, 并且需要支持异步验证

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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
const ValidateStatuses = ['success', 'warning', 'error', 'validating', ''] as const;

export type FormValueType = string | number | boolean;
export type FormFieldState = {
status: typeof ValidateStatuses[number];
help: string;
promise?: Promise<RuleResultValue>;
};
export type RuleResultValue = string | boolean | FormFieldState;
export type RuleResult = RuleResultValue | Promise<RuleResultValue>;

export type FormFieldsState<T> = { [key in keyof T]: FormFieldState };

export type FormFieldInfoState<T> = {
status: typeof ValidateStatuses[number];
fields: FormFieldsState<T>;
};

const isPromise = <T>(value: T | Promise<T>): value is Promise<T> => {
return _.hasIn(value, 'then');
};

const undefinedToFalse = (value: RuleResultValue) => {
return _.isUndefined(value) ? false : value;
};

const ruleResultValueToFormFieldState = (value: RuleResultValue): FormFieldState => {
value = undefinedToFalse(value);
let result: FormFieldState = { status: '', help: undefined };
if (_.isObject(value) && _.has(value, ['status', 'success', 'help'])) {
result = value;
}
if (_.isString(value) || _.isBoolean(value)) {
result = {
status: !value ? 'success' : 'error',
help: _.isString(value) ? value : '',
};
}
return result;
};

const getAllStatusFromFieldStatus = (allStatus: typeof ValidateStatuses[number][]) => {
if (allStatus.includes('')) {
return '';
}
if (allStatus.includes('validating')) {
return 'validating';
}
if (allStatus.includes('error')) {
return 'error';
}
if (allStatus.includes('warning')) {
return 'warning';
}
return 'success';
};

const setAllFieldsStatus = <
T extends {
[key: string]: FormValueType;
},
>(
formFieldInfoState: FormFieldInfoState<T>,
allRuleKeys: string[],
): FormFieldInfoState<T> => {
const allStatus = _.map(allRuleKeys, (key) => formFieldInfoState.fields[key].status);
formFieldInfoState.status = getAllStatusFromFieldStatus(allStatus);
return formFieldInfoState;
};

const validateField = async <
T extends {
[key: string]: FormValueType;
},
>(
field: keyof T,
formRules: { [key in keyof T]: () => RuleResult },
dispatch: React.Dispatch<
| {
type: 'INIT';
}
| {
type: 'CHANGE';
payload: {
field: keyof T;
result: RuleResult;
allRuleKeys: string[];
};
}
| {
type: 'PROMISE_DONE';
payload: {
field: keyof T;
result: RuleResult;
promise: Promise<RuleResultValue>;
allRuleKeys: string[];
};
}
>,
): Promise<FormFieldState> => {
const allRuleKeys = Object.keys(formRules);
const validateResult = formRules[field]();
dispatch({
type: 'CHANGE',
payload: {
allRuleKeys: allRuleKeys,
field: field,
result: validateResult,
},
});
let result: RuleResultValue = null;
if (isPromise(validateResult)) {
try {
result = await validateResult;
dispatch({
type: 'PROMISE_DONE',
payload: {
allRuleKeys: allRuleKeys,
field: field,
result: result,
promise: validateResult,
},
});
} catch (error) {
console.log(`validate field error: ${field}`, error);
}
} else {
result = validateResult;
}
return ruleResultValueToFormFieldState(result);
};

const useFormValidator = (
form: {
[key: string]: FormValueType;
},
formRules: { [key in keyof typeof form]: () => RuleResult },
) => {
type FormType = typeof form;
type FormKeys = keyof FormType;
const allRuleKeys = Object.keys(formRules);

const [formFieldInfoState, dispatchFormFieldInfoState] = useReducer<
React.Reducer<
FormFieldInfoState<FormType>,
| {
type: 'INIT';
}
| {
type: 'CHANGE';
payload: {
field: FormKeys;
result: RuleResult;
allRuleKeys: string[];
};
}
| {
type: 'PROMISE_DONE';
payload: {
field: FormKeys;
result: RuleResult;
promise: Promise<RuleResultValue>;
allRuleKeys: string[];
};
}
>
>(
(state, action) => {
const { type } = action;
switch (action.type) {
case 'INIT':
return {
fields: _.transform<string, FormFieldsState<FormType>>(
allRuleKeys,
(result, key) => {
_.set(result, key, {
status: '',
help: undefined,
});
},
{},
),
status: '',
};
case 'CHANGE':
if (isPromise(action.payload.result)) {
_.set(state.fields, action.payload.field, {
status: 'validating',
help: undefined,
promise: action.payload.result,
});
} else {
_.set(state.fields, action.payload.field, ruleResultValueToFormFieldState(action.payload.result));
}
return { ...setAllFieldsStatus(state, action.payload.allRuleKeys) };
case 'PROMISE_DONE':
if (!isPromise(action.payload.result)) {
if (action.payload.promise === _.get(state.fields, action.payload.field)?.promise) {
_.set(state.fields, action.payload.field, ruleResultValueToFormFieldState(action.payload.result));
return { ...setAllFieldsStatus(state, action.payload.allRuleKeys) };
}
}
return state;
default:
throw new Error(`action: ${type} is error`);
}
},
{
fields: _.transform<string, FormFieldsState<FormType>>(
allRuleKeys,
(result, key) => {
_.set(result, key, {
status: '',
help: undefined,
});
},
{},
),
status: '',
},
);

function getFieldStatus(): Promise<FormFieldInfoState<FormType>>;
function getFieldStatus(key: FormKeys): Promise<FormFieldState>;
async function getFieldStatus(key?: FormKeys): Promise<FormFieldState | FormFieldInfoState<FormType>> {
if (key && _.has(formRules, key)) {
return await validateField(key, formRules, dispatchFormFieldInfoState);
}
if (!key) {
const ruleKeys = Object.keys(formRules);
return await Promise.all(ruleKeys.map(async (x) => ({ [x]: await validateField(x, formRules, dispatchFormFieldInfoState) }))).then(
(_formFieldInfoState: { [x: string]: FormFieldState }[]) =>
setAllFieldsStatus({ status: '', fields: Object.assign({}, ..._formFieldInfoState) }, ruleKeys),
);
}
}
const setInit = () => dispatchFormFieldInfoState({ type: 'INIT' });
return { formFieldInfoState, getFieldStatus, setInit };
};

export default useFormValidator;

使用

上面Hooks开发完后在函数式组件里使用表单就非常简单了

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
const FormComponent: React.FC = () => {
const [{ form, formFieldInfoState }, { formInputHandler, changeHandlerBuild, getFieldStatus }] = useForm(
{
type: '',
name: '',
sub: {
prop: false,
},
},
{
type() {
if (!form.type) {
return 'type is required';
}
},
async name() {
if (!form.name) {
return 'name is required';
}
},
['sub.prop']() {
if (!form.sub.prop) {
return 'sub.prop is required';
}
},
},
);

const clickSaveForm = async () => {
if ((await getFieldStatus()).status !== 'success') {
return;
}
await setSavePromise(
UTILS.post(api(), {
...form,
}),
);
message.success(t('COMMON_SUCCESS'));
};

return (
<form className='ant-form ant-form-horizontal'>
<Form.Item
label='type'
labelAlign='left'
required
validateStatus={formFieldInfoState.fields.type.status}
help={formFieldInfoState.fields.type.help}
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
hasFeedback
>
<Input {...formInputHandler('type')} />
</Form.Item>
<Form.Item
label={'sub.prop'}
help={_.get(formFieldInfoState.fields, 'sub.prop').help}
validateStatus={_.get(formFieldInfoState.fields, 'sub.prop').status}
labelCol={{ span: 8 }}
wrapperCol={{ span: 6 }}
>
<MultiSelect<number>
placeholder='sub.prop'
mode='tags'
value={form.sub.prop}
options={_.map(CONSTANT.OPTIONS, (x) => ({ label: t(x.label), value: x.value }))}
onChange={changeHandlerBuild('sub.prop', (value) => value)}
/>
</Form.Item>
</form>
);
};
作者

Mosby

发布于

2019-03-21

许可协议

评论