taro-form-react 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -0
- package/.eslintrc +233 -0
- package/dist/index.js +21 -0
- package/package.json +56 -0
- package/rollup.config.mjs +53 -0
- package/src/components/Item.tsx +209 -0
- package/src/components/Keep.tsx +28 -0
- package/src/components/Label.tsx +96 -0
- package/src/components/Provider.tsx +15 -0
- package/src/context/FormContext.tsx +441 -0
- package/src/hooks/useFormConfiguration.ts +37 -0
- package/src/hooks/useFormItem.ts +75 -0
- package/src/hooks/useMap.ts +36 -0
- package/src/index.tsx +99 -0
- package/src/styles/index.scss +87 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/rules.ts +55 -0
- package/src/utils/tools.ts +31 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { cloneDeep, get, merge, set, unset } from "lodash-es";
|
|
4
|
+
|
|
5
|
+
import useMap from "@/hooks/useMap";
|
|
6
|
+
|
|
7
|
+
import type { FormLabelProps } from "../components/Label";
|
|
8
|
+
import type { NamePath } from "../types";
|
|
9
|
+
|
|
10
|
+
import { namePathToString } from "../utils";
|
|
11
|
+
|
|
12
|
+
export type FieldTransformResult = {
|
|
13
|
+
__form_internals_should_merge?: boolean;
|
|
14
|
+
value: any;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type FieldRefActions = {
|
|
18
|
+
validate: () => (void | string[] | Promise<void | string[]>);
|
|
19
|
+
// transform 会将返回的对象 merge 到整个表单的 data 中
|
|
20
|
+
// 如果设置了 transform 且 __form_internals_should_merge 为 true
|
|
21
|
+
// 那么 field 的 name 会被忽略(仅在使用 getFieldsFormattedValue 时有效)
|
|
22
|
+
// 否则会直接使用返回的值
|
|
23
|
+
transform?: () => FieldTransformResult | Promise<FieldTransformResult>;
|
|
24
|
+
};
|
|
25
|
+
export type FieldRef = React.RefObject<FieldRefActions>;
|
|
26
|
+
|
|
27
|
+
export type Field = {
|
|
28
|
+
refs: FieldRef[];
|
|
29
|
+
name: NamePath;
|
|
30
|
+
touched: boolean;
|
|
31
|
+
errors: string[];
|
|
32
|
+
initialValue?: any;
|
|
33
|
+
count: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type FormContextProps = {
|
|
37
|
+
initialValues?: Record<string, any>;
|
|
38
|
+
|
|
39
|
+
showError?: boolean;
|
|
40
|
+
colon?: boolean;
|
|
41
|
+
labelProps?: FormLabelProps;
|
|
42
|
+
layout?: "horizontal" | "vertical";
|
|
43
|
+
validateFirst?: boolean | "parallel";
|
|
44
|
+
showErrors?: boolean;
|
|
45
|
+
passthroughErrors?: boolean;
|
|
46
|
+
// merge 会将 transform 返回的对象 merge 到整个表单的 data 中
|
|
47
|
+
// replace 会直接使用 transform 返回的值填充到对应的 field 中
|
|
48
|
+
transformBehavior?: "merge" | "replace";
|
|
49
|
+
getRequiredMessage?: (label: string) => string;
|
|
50
|
+
|
|
51
|
+
data: Record<string, any>;
|
|
52
|
+
setData: (data: Record<string, any>) => void;
|
|
53
|
+
|
|
54
|
+
fields: Map<string, Field>;
|
|
55
|
+
registerField: (name: NamePath, ref: FieldRef, initialValue?: any) => void;
|
|
56
|
+
unregisterField: (name: NamePath, ref: FieldRef) => void;
|
|
57
|
+
|
|
58
|
+
setFieldValue: (name: NamePath, value: any) => void;
|
|
59
|
+
getFieldValue: (name: NamePath) => any;
|
|
60
|
+
getFieldsValue: (nameList?: NamePath[]) => Record<string, any>;
|
|
61
|
+
getFieldsFormattedValue: (nameList?: NamePath[]) => Promise<Record<string, any>>;
|
|
62
|
+
|
|
63
|
+
setFields: (
|
|
64
|
+
fields: Array<
|
|
65
|
+
Pick<Field, "name">
|
|
66
|
+
& { touched?: boolean; }
|
|
67
|
+
& { value?: any }
|
|
68
|
+
>
|
|
69
|
+
) => void;
|
|
70
|
+
getFields: (nameList?: NamePath[]) => Array<
|
|
71
|
+
Pick<Field, "name" | "touched" | "errors">
|
|
72
|
+
& { value: any }
|
|
73
|
+
>;
|
|
74
|
+
|
|
75
|
+
resetFields: (nameList?: NamePath[]) => void;
|
|
76
|
+
|
|
77
|
+
setFieldError: (name: NamePath, errors: string[]) => void;
|
|
78
|
+
getFieldError: (name: NamePath) => string[];
|
|
79
|
+
|
|
80
|
+
validateFields: (nameList?: NamePath[]) => Promise<void | Pick<Field, "name" | "errors">[]>;
|
|
81
|
+
|
|
82
|
+
isFieldsTouched: (nameList?: NamePath[], options?: { allTouched?: boolean }) => boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const FormContext = createContext<FormContextProps | undefined>(void 0);
|
|
86
|
+
|
|
87
|
+
export const useFormContext = () => {
|
|
88
|
+
const context = useContext(FormContext);
|
|
89
|
+
if (!context) {
|
|
90
|
+
throw new Error("useFormContext must be used within a FormProvider");
|
|
91
|
+
}
|
|
92
|
+
return context;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type FormProviderConfiguration = Pick<FormContextProps, "colon" | "labelProps" | "layout" | "validateFirst" | "showErrors" | "passthroughErrors" | "transformBehavior" | "getRequiredMessage">;
|
|
96
|
+
|
|
97
|
+
export type FormProviderProps =
|
|
98
|
+
& FormProviderConfiguration
|
|
99
|
+
& Pick<FormContextProps, "initialValues">
|
|
100
|
+
& {
|
|
101
|
+
onFieldsChange?: (changedFields: Array<{ name: NamePath; value: any }>, allFields: Array<{ name: NamePath; value: any }>) => void;
|
|
102
|
+
onValuesChange?: (changedValues: Record<string, any>, allValues: Record<string, any>) => void;
|
|
103
|
+
children: React.ReactNode;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const FormContextProvider: React.FC<FormProviderProps> = ({
|
|
107
|
+
initialValues,
|
|
108
|
+
onFieldsChange,
|
|
109
|
+
onValuesChange,
|
|
110
|
+
children,
|
|
111
|
+
...props
|
|
112
|
+
}) => {
|
|
113
|
+
const [fields, { set: setField, get: getField, remove: removeField }] = useMap<string, Field>();
|
|
114
|
+
const [data, setData] = useState<Record<string, any>>({});
|
|
115
|
+
|
|
116
|
+
const registerField = useCallback<FormContextProps["registerField"]>((name, ref, initialValue) => {
|
|
117
|
+
const nameString = namePathToString(name);
|
|
118
|
+
const value = initialValue ?? get(initialValues, name);
|
|
119
|
+
setData(data => {
|
|
120
|
+
const nextData = cloneDeep(data);
|
|
121
|
+
set(nextData, name, value);
|
|
122
|
+
return nextData;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const field = getField(nameString);
|
|
126
|
+
if (field) {
|
|
127
|
+
console.warn(`Field "${nameString}" is already registered, register multiple times may cause unexpected behavior.`);
|
|
128
|
+
}
|
|
129
|
+
const fieldToSet = field
|
|
130
|
+
? {
|
|
131
|
+
...field,
|
|
132
|
+
refs: [...field.refs, ref],
|
|
133
|
+
initialValue,
|
|
134
|
+
count: field.count + 1,
|
|
135
|
+
}
|
|
136
|
+
: {
|
|
137
|
+
refs: [ref],
|
|
138
|
+
name,
|
|
139
|
+
touched: false,
|
|
140
|
+
errors: [],
|
|
141
|
+
initialValue,
|
|
142
|
+
count: 1,
|
|
143
|
+
};
|
|
144
|
+
setField(nameString, fieldToSet);
|
|
145
|
+
|
|
146
|
+
onFieldsChange?.(
|
|
147
|
+
[{ name, value }],
|
|
148
|
+
Array.from(fields.values()).map(field => ({ name: field.name, value: get(data, field.name) })).concat([{ name, value }]),
|
|
149
|
+
);
|
|
150
|
+
}, [data, fields, getField, initialValues, onFieldsChange, setField]);
|
|
151
|
+
|
|
152
|
+
const unregisterField = useCallback<FormContextProps["unregisterField"]>((name, ref) => {
|
|
153
|
+
const nameString = namePathToString(name);
|
|
154
|
+
const field = getField(nameString);
|
|
155
|
+
if (field) {
|
|
156
|
+
if (field.count === 1) {
|
|
157
|
+
removeField(nameString);
|
|
158
|
+
setData(data => {
|
|
159
|
+
const nextData = cloneDeep(data);
|
|
160
|
+
unset(nextData, name);
|
|
161
|
+
return nextData;
|
|
162
|
+
});
|
|
163
|
+
onFieldsChange?.(
|
|
164
|
+
[{ name, value: get(data, name) }],
|
|
165
|
+
Array.from(fields.values()).map(field => ({ name: field.name, value: get(data, field.name) })).filter(f => f.name !== name),
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
setField(nameString, {
|
|
169
|
+
...field,
|
|
170
|
+
count: field.count - 1,
|
|
171
|
+
refs: field.refs.filter(r => r !== ref),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
console.warn(`Attempted to unregister field "${nameString}" that was never registered.`);
|
|
176
|
+
}
|
|
177
|
+
}, [data, fields, getField, onFieldsChange, removeField, setField]);
|
|
178
|
+
|
|
179
|
+
const setFieldValue = useCallback<FormContextProps["setFieldValue"]>((name, value) => {
|
|
180
|
+
const nameString = namePathToString(name);
|
|
181
|
+
const field = getField(nameString);
|
|
182
|
+
if (!field) {
|
|
183
|
+
console.warn(`Attempted to set value for field "${nameString}" that was never registered.`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setData(data => {
|
|
188
|
+
const nextData = cloneDeep(data);
|
|
189
|
+
set(nextData, name, value);
|
|
190
|
+
return nextData;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
setField(nameString, { ...field, touched: true });
|
|
194
|
+
|
|
195
|
+
if (onValuesChange) {
|
|
196
|
+
const changes = {};
|
|
197
|
+
set(changes, name, value);
|
|
198
|
+
const all = cloneDeep(data);
|
|
199
|
+
set(all, name, value);
|
|
200
|
+
onValuesChange(changes, all);
|
|
201
|
+
}
|
|
202
|
+
}, [data, getField, onValuesChange, setField]);
|
|
203
|
+
|
|
204
|
+
const getFieldValue = useCallback<FormContextProps["getFieldValue"]>(name => {
|
|
205
|
+
return get(data, name);
|
|
206
|
+
}, [data]);
|
|
207
|
+
|
|
208
|
+
const getFieldsValue = useCallback<FormContextProps["getFieldsValue"]>(nameList => {
|
|
209
|
+
if (!nameList) return data;
|
|
210
|
+
|
|
211
|
+
const result = {} as Record<string, any>;
|
|
212
|
+
for (const name of nameList) {
|
|
213
|
+
const value = get(data, name);
|
|
214
|
+
set(result, name, value);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}, [data]);
|
|
218
|
+
|
|
219
|
+
const getFieldsFormattedValue = useCallback<FormContextProps["getFieldsFormattedValue"]>(async nameList => {
|
|
220
|
+
const realNameList = nameList ?? Array.from(fields.values()).map(field => field.name);
|
|
221
|
+
|
|
222
|
+
const result = {} as Record<string, any>;
|
|
223
|
+
|
|
224
|
+
for (const name of realNameList) {
|
|
225
|
+
const field = getField(namePathToString(name));
|
|
226
|
+
if (!field) {
|
|
227
|
+
console.warn(`Attempted to get formatted value for field "${namePathToString(name)}" that was never registered.`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
field.refs.some(ref => !ref.current)
|
|
233
|
+
) {
|
|
234
|
+
console.warn(`Attempted to get formatted value for field "${namePathToString(name)}" that has no or missing ref.`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// only consider the last ref's transform result
|
|
239
|
+
const transformFunc = field.refs[field.refs.length - 1].current!.transform;
|
|
240
|
+
if (!transformFunc) {
|
|
241
|
+
set(result, name, get(data, name));
|
|
242
|
+
} else {
|
|
243
|
+
const transformedValue = await transformFunc();
|
|
244
|
+
if (transformedValue.__form_internals_should_merge) {
|
|
245
|
+
merge(result, transformedValue.value);
|
|
246
|
+
} else {
|
|
247
|
+
set(result, name, transformedValue);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return result;
|
|
253
|
+
}, [data, fields, getField]);
|
|
254
|
+
|
|
255
|
+
const setFields = useCallback<FormContextProps["setFields"]>(fields => {
|
|
256
|
+
const changes = {};
|
|
257
|
+
for (const fieldData of fields) {
|
|
258
|
+
const nameString = namePathToString(fieldData.name);
|
|
259
|
+
const field = getField(namePathToString(fieldData.name));
|
|
260
|
+
if (!field) {
|
|
261
|
+
console.warn(`Attempted to set field "${nameString}" that was never registered.`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (fieldData.touched !== undefined) {
|
|
266
|
+
setField(nameString, { ...field, touched: fieldData.touched });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
setData(data => {
|
|
270
|
+
const nextData = cloneDeep(data);
|
|
271
|
+
set(nextData, fieldData.name, fieldData.value);
|
|
272
|
+
set(changes, fieldData.name, fieldData.value);
|
|
273
|
+
return nextData;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
onValuesChange?.(changes, merge(cloneDeep(data), changes));
|
|
277
|
+
}, [data, getField, onValuesChange, setField]);
|
|
278
|
+
|
|
279
|
+
const getFields = useCallback<FormContextProps["getFields"]>(nameList => {
|
|
280
|
+
if (!nameList) {
|
|
281
|
+
return Array
|
|
282
|
+
.from(fields.values())
|
|
283
|
+
.map(field => ({ ...field, value: get(data, field.name) }));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const result = [] as ReturnType<FormContextProps["getFields"]>;
|
|
287
|
+
for (const name of nameList) {
|
|
288
|
+
const field = getField(namePathToString(name));
|
|
289
|
+
if (!field) {
|
|
290
|
+
console.warn(`Attempted to get field "${namePathToString(name)}" that was never registered.`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const value = get(data, name);
|
|
295
|
+
result.push({ ...field, value });
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}, [data, fields, getField]);
|
|
299
|
+
|
|
300
|
+
const resetFields = useCallback<FormContextProps["resetFields"]>(nameList => {
|
|
301
|
+
const fieldsToReset =
|
|
302
|
+
nameList
|
|
303
|
+
?.map(name => {
|
|
304
|
+
const nameString = namePathToString(name);
|
|
305
|
+
const field = getField(nameString);
|
|
306
|
+
if (!field) {
|
|
307
|
+
console.warn(`Attempted to reset field "${nameString}" that was never registered.`);
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
return field;
|
|
311
|
+
})
|
|
312
|
+
.filter(v => v !== undefined)
|
|
313
|
+
?? Array.from(fields.values());
|
|
314
|
+
|
|
315
|
+
for (const field of fieldsToReset) {
|
|
316
|
+
setData(data => {
|
|
317
|
+
const nextData = cloneDeep(data);
|
|
318
|
+
set(nextData, field.name, field.initialValue ?? get(initialValues, field.name));
|
|
319
|
+
return nextData;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
setField(namePathToString(field.name), { ...field, touched: false });
|
|
323
|
+
}
|
|
324
|
+
}, [fields, getField, initialValues, setField]);
|
|
325
|
+
|
|
326
|
+
const setFieldError = useCallback<FormContextProps["setFieldError"]>((name, errors) => {
|
|
327
|
+
const nameString = namePathToString(name);
|
|
328
|
+
const field = getField(nameString);
|
|
329
|
+
if (!field) {
|
|
330
|
+
console.warn(`Attempted to set errors for field "${nameString}" that was never registered.`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
setField(nameString, { ...field, errors });
|
|
335
|
+
}, [getField, setField]);
|
|
336
|
+
|
|
337
|
+
const getFieldError = useCallback<FormContextProps["getFieldError"]>(name => {
|
|
338
|
+
const field = getField(namePathToString(name));
|
|
339
|
+
if (!field) {
|
|
340
|
+
console.warn(`Attempted to get errors for field "${namePathToString(name)}" that was never registered.`);
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
return field.errors;
|
|
344
|
+
}, [getField]);
|
|
345
|
+
|
|
346
|
+
const validateFields = useCallback<FormContextProps["validateFields"]>(async nameList => {
|
|
347
|
+
const fieldsToValidate =
|
|
348
|
+
nameList
|
|
349
|
+
?.map(name => {
|
|
350
|
+
const nameString = namePathToString(name);
|
|
351
|
+
const field = getField(nameString);
|
|
352
|
+
if (!field) {
|
|
353
|
+
console.warn(`Attempted to validate field "${nameString}" that was never registered.`);
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
return field;
|
|
357
|
+
})
|
|
358
|
+
.filter(v => v !== undefined)
|
|
359
|
+
?? Array.from(fields.values());
|
|
360
|
+
|
|
361
|
+
const errors = [] as Pick<Field, "name" | "errors">[];
|
|
362
|
+
|
|
363
|
+
for (const field of fieldsToValidate) {
|
|
364
|
+
if (
|
|
365
|
+
field.refs.some(ref => !ref.current)
|
|
366
|
+
) {
|
|
367
|
+
console.warn(`Attempted to validate field "${namePathToString(field.name)}" that has no or missing ref.`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const result = await Promise.all(field.refs.map(ref => ref.current!.validate()));
|
|
372
|
+
const resultErrors = Array.from(new Set(result.flat().filter(Boolean))) as string[];
|
|
373
|
+
if (resultErrors.length > 0) {
|
|
374
|
+
errors.push({
|
|
375
|
+
name: field.name,
|
|
376
|
+
errors: resultErrors,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return errors.length > 0 ? errors : void 0;
|
|
382
|
+
}, [fields, getField]);
|
|
383
|
+
|
|
384
|
+
const isFieldsTouched = useCallback<FormContextProps["isFieldsTouched"]>((nameList, options) => {
|
|
385
|
+
const { allTouched = false } = options ?? {};
|
|
386
|
+
|
|
387
|
+
const fieldsToCheck =
|
|
388
|
+
nameList
|
|
389
|
+
?.map(name => {
|
|
390
|
+
const nameString = namePathToString(name);
|
|
391
|
+
const field = getField(nameString);
|
|
392
|
+
if (!field) {
|
|
393
|
+
console.warn(`Attempted to check if field "${nameString}" is touched but it was never registered.`);
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
return field;
|
|
397
|
+
})
|
|
398
|
+
.filter(v => v !== undefined)
|
|
399
|
+
?? Array.from(fields.values());
|
|
400
|
+
|
|
401
|
+
return allTouched
|
|
402
|
+
? fieldsToCheck.every(field => field.touched)
|
|
403
|
+
: fieldsToCheck.some(field => field.touched);
|
|
404
|
+
}, [fields, getField]);
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<FormContext.Provider
|
|
408
|
+
value={{
|
|
409
|
+
initialValues,
|
|
410
|
+
|
|
411
|
+
...props,
|
|
412
|
+
|
|
413
|
+
data,
|
|
414
|
+
setData,
|
|
415
|
+
|
|
416
|
+
fields,
|
|
417
|
+
registerField,
|
|
418
|
+
unregisterField,
|
|
419
|
+
|
|
420
|
+
setFieldValue,
|
|
421
|
+
getFieldValue,
|
|
422
|
+
getFieldsValue,
|
|
423
|
+
getFieldsFormattedValue,
|
|
424
|
+
|
|
425
|
+
setFields,
|
|
426
|
+
getFields,
|
|
427
|
+
|
|
428
|
+
resetFields,
|
|
429
|
+
|
|
430
|
+
setFieldError,
|
|
431
|
+
getFieldError,
|
|
432
|
+
|
|
433
|
+
validateFields,
|
|
434
|
+
|
|
435
|
+
isFieldsTouched,
|
|
436
|
+
}}
|
|
437
|
+
>
|
|
438
|
+
{children}
|
|
439
|
+
</FormContext.Provider>
|
|
440
|
+
);
|
|
441
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FormProviderConfiguration } from "../context/FormContext";
|
|
2
|
+
|
|
3
|
+
import { useFormContext } from "../context/FormContext";
|
|
4
|
+
|
|
5
|
+
const fallback = (value: any, fallbackValue: any) => (value === undefined ? fallbackValue : value);
|
|
6
|
+
|
|
7
|
+
export default function useFormConfiguration ({
|
|
8
|
+
colon,
|
|
9
|
+
labelProps,
|
|
10
|
+
layout,
|
|
11
|
+
validateFirst,
|
|
12
|
+
passthroughErrors,
|
|
13
|
+
showErrors,
|
|
14
|
+
getRequiredMessage,
|
|
15
|
+
}: Partial<FormProviderConfiguration> = {}) {
|
|
16
|
+
const {
|
|
17
|
+
colon: contextColon = true,
|
|
18
|
+
labelProps: contextLabelProps = {},
|
|
19
|
+
layout: contextLayout = "vertical",
|
|
20
|
+
validateFirst: contextValidateFirst = false,
|
|
21
|
+
passthroughErrors: contextPassthroughErrors = false,
|
|
22
|
+
showErrors: contextShowErrors = true,
|
|
23
|
+
transformBehavior: contextTransformBehavior = "replace",
|
|
24
|
+
getRequiredMessage: contextGetRequiredMessage = label => `${label}不能为空`,
|
|
25
|
+
} = useFormContext();
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
colon: fallback(colon, contextColon),
|
|
29
|
+
labelProps: fallback(labelProps, contextLabelProps),
|
|
30
|
+
layout: fallback(layout, contextLayout),
|
|
31
|
+
validateFirst: fallback(validateFirst, contextValidateFirst),
|
|
32
|
+
passthroughErrors: fallback(passthroughErrors, contextPassthroughErrors),
|
|
33
|
+
showErrors: fallback(showErrors, contextShowErrors),
|
|
34
|
+
transformBehavior: contextTransformBehavior,
|
|
35
|
+
getRequiredMessage: fallback(getRequiredMessage, contextGetRequiredMessage),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import { useDeepCompareEffect } from "ahooks";
|
|
4
|
+
import { get } from "lodash-es";
|
|
5
|
+
|
|
6
|
+
import type { FieldRef } from "../context/FormContext";
|
|
7
|
+
import type { NamePath } from "../types";
|
|
8
|
+
|
|
9
|
+
import { useFormContext } from "../context/FormContext";
|
|
10
|
+
|
|
11
|
+
export type UseFormItemProps = {
|
|
12
|
+
ref: FieldRef;
|
|
13
|
+
name: NamePath;
|
|
14
|
+
initialValue?: any;
|
|
15
|
+
dependencies?: NamePath[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type UseFormItemResult = {
|
|
19
|
+
value: any;
|
|
20
|
+
errors: string[];
|
|
21
|
+
isTouched: boolean;
|
|
22
|
+
dependencyValues: any[];
|
|
23
|
+
onChange: (value: any) => void;
|
|
24
|
+
onErrorsChange: (errors: string[]) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default function useFormItem ({
|
|
28
|
+
ref,
|
|
29
|
+
name,
|
|
30
|
+
initialValue,
|
|
31
|
+
dependencies,
|
|
32
|
+
}: UseFormItemProps): UseFormItemResult {
|
|
33
|
+
const isFieldRegistered = useRef(false);
|
|
34
|
+
const { data, registerField, unregisterField, setFieldValue, setFieldError, getFieldError, isFieldsTouched } = useFormContext();
|
|
35
|
+
|
|
36
|
+
useDeepCompareEffect(() => {
|
|
37
|
+
const currentName = name;
|
|
38
|
+
const currentRef = ref;
|
|
39
|
+
registerField(currentName, currentRef, initialValue);
|
|
40
|
+
isFieldRegistered.current = true;
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
unregisterField(currentName, currentRef);
|
|
44
|
+
isFieldRegistered.current = false;
|
|
45
|
+
};
|
|
46
|
+
}, [name]);
|
|
47
|
+
|
|
48
|
+
const value = useMemo(() => {
|
|
49
|
+
return get(data, name);
|
|
50
|
+
}, [data, name]);
|
|
51
|
+
|
|
52
|
+
const errors = isFieldRegistered.current ? getFieldError(name) : [];
|
|
53
|
+
const isTouched = isFieldRegistered.current ? isFieldsTouched([name]) : false;
|
|
54
|
+
|
|
55
|
+
const onChange = useCallback((value: any) => {
|
|
56
|
+
setFieldValue(name, value);
|
|
57
|
+
}, [name, setFieldValue]);
|
|
58
|
+
|
|
59
|
+
const onErrorsChange = useCallback((errors: string[]) => {
|
|
60
|
+
setFieldError(name, errors);
|
|
61
|
+
}, [name, setFieldError]);
|
|
62
|
+
|
|
63
|
+
const dependencyValues = useMemo(() => {
|
|
64
|
+
return (dependencies || []).map(dep => get(data, dep));
|
|
65
|
+
}, [data, dependencies]);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
value,
|
|
69
|
+
errors,
|
|
70
|
+
isTouched,
|
|
71
|
+
onChange,
|
|
72
|
+
onErrorsChange,
|
|
73
|
+
dependencyValues,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import { useUpdate } from "ahooks";
|
|
4
|
+
|
|
5
|
+
export default function useMap<
|
|
6
|
+
K extends string | number | symbol,
|
|
7
|
+
V,
|
|
8
|
+
> () {
|
|
9
|
+
const map = useRef(new Map<K, V>());
|
|
10
|
+
|
|
11
|
+
const update = useUpdate();
|
|
12
|
+
|
|
13
|
+
const set = useCallback((key: K, value: V) => {
|
|
14
|
+
map.current.set(key, value);
|
|
15
|
+
update();
|
|
16
|
+
}, [update]);
|
|
17
|
+
|
|
18
|
+
const setAll = useCallback((newMap: Iterable<readonly [K, V]>) => {
|
|
19
|
+
map.current = new Map(newMap);
|
|
20
|
+
update();
|
|
21
|
+
}, [update]);
|
|
22
|
+
|
|
23
|
+
const remove = useCallback((key: K) => {
|
|
24
|
+
map.current.delete(key);
|
|
25
|
+
update();
|
|
26
|
+
}, [update]);
|
|
27
|
+
|
|
28
|
+
const reset = useCallback(() => {
|
|
29
|
+
map.current.clear();
|
|
30
|
+
update();
|
|
31
|
+
}, [update]);
|
|
32
|
+
|
|
33
|
+
const get = useCallback((key: K) => map.current.get(key), []);
|
|
34
|
+
|
|
35
|
+
return [map.current, { set, setAll, remove, reset, get }] as const;
|
|
36
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useImperativeHandle } from "react";
|
|
2
|
+
|
|
3
|
+
import FormItem from "./components/Item";
|
|
4
|
+
import FormKeep from "./components/Keep";
|
|
5
|
+
import FormLabel from "./components/Label";
|
|
6
|
+
import FormProvider from "./components/Provider";
|
|
7
|
+
|
|
8
|
+
import { filterUndefined } from "@/utils/tools";
|
|
9
|
+
|
|
10
|
+
import type { Field, FormContextProps, FormProviderConfiguration } from "./context/FormContext";
|
|
11
|
+
|
|
12
|
+
import { FormContextProvider, useFormContext } from "./context/FormContext";
|
|
13
|
+
|
|
14
|
+
export type FormActions =
|
|
15
|
+
Pick<FormContextProps, "setFieldValue" | "getFieldValue" | "getFieldsValue" | "setFields" | "getFields" | "resetFields" | "setFieldError" | "getFieldError" | "validateFields" | "isFieldsTouched">
|
|
16
|
+
& {
|
|
17
|
+
submit: () => Promise<Record<string, any> | undefined>;
|
|
18
|
+
reset: () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type FormProps =
|
|
22
|
+
& FormProviderConfiguration
|
|
23
|
+
& {
|
|
24
|
+
// goes to FormProvider
|
|
25
|
+
initialValues?: Record<string, any>;
|
|
26
|
+
onFieldsChange?: (changedFields: Array<{ name: string[]; value: any }>, allFields: Array<{ name: string[]; value: any }>) => void;
|
|
27
|
+
onValuesChange?: (changedValues: Record<string, any>, allValues: Record<string, any>) => void;
|
|
28
|
+
|
|
29
|
+
// goes to InnerForm
|
|
30
|
+
omitNil?: boolean;
|
|
31
|
+
onFinish?: (values: Record<string, any>) => void;
|
|
32
|
+
onFinishFailed?: (errors: Pick<Field, "name" | "errors">[]) => void;
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const InnerForm = forwardRef<FormActions, FormProps>(({
|
|
37
|
+
omitNil = true,
|
|
38
|
+
onFinish,
|
|
39
|
+
onFinishFailed,
|
|
40
|
+
children,
|
|
41
|
+
}, ref) => {
|
|
42
|
+
const context = useFormContext();
|
|
43
|
+
|
|
44
|
+
const handleSubmit = useCallback(async () => {
|
|
45
|
+
const errors = await context.validateFields();
|
|
46
|
+
if (errors) {
|
|
47
|
+
onFinishFailed?.(errors);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const values = omitNil ? filterUndefined(context.getFieldsValue(), true) : context.getFieldsValue();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
onFinish?.(values);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.log(e);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return values;
|
|
59
|
+
}, [context, omitNil, onFinish, onFinishFailed]);
|
|
60
|
+
|
|
61
|
+
useImperativeHandle(ref, () => ({
|
|
62
|
+
submit: handleSubmit,
|
|
63
|
+
reset: context.resetFields,
|
|
64
|
+
setFieldValue: context.setFieldValue,
|
|
65
|
+
getFieldValue: context.getFieldValue,
|
|
66
|
+
getFieldsValue: context.getFieldsValue,
|
|
67
|
+
setFields: context.setFields,
|
|
68
|
+
getFields: context.getFields,
|
|
69
|
+
resetFields: context.resetFields,
|
|
70
|
+
setFieldError: context.setFieldError,
|
|
71
|
+
getFieldError: context.getFieldError,
|
|
72
|
+
validateFields: context.validateFields,
|
|
73
|
+
isFieldsTouched: context.isFieldsTouched,
|
|
74
|
+
}), [context.getFieldError, context.getFieldValue, context.getFields, context.getFieldsValue, context.isFieldsTouched, context.resetFields, context.setFieldError, context.setFieldValue, context.setFields, context.validateFields, handleSubmit]);
|
|
75
|
+
|
|
76
|
+
return children;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
interface FormComponent extends React.ForwardRefExoticComponent<FormProps & React.RefAttributes<FormActions>> {
|
|
80
|
+
Item: typeof FormItem;
|
|
81
|
+
Provider: typeof FormProvider;
|
|
82
|
+
Label: typeof FormLabel;
|
|
83
|
+
Keep: typeof FormKeep;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const Form = forwardRef<FormActions, FormProps>((props, ref) => {
|
|
87
|
+
return (
|
|
88
|
+
<FormContextProvider {...props}>
|
|
89
|
+
<InnerForm ref={ref} {...props} />
|
|
90
|
+
</FormContextProvider>
|
|
91
|
+
);
|
|
92
|
+
}) as FormComponent;
|
|
93
|
+
|
|
94
|
+
Form.Item = FormItem;
|
|
95
|
+
Form.Provider = FormProvider;
|
|
96
|
+
Form.Label = FormLabel;
|
|
97
|
+
Form.Keep = FormKeep;
|
|
98
|
+
|
|
99
|
+
export default Form;
|