@rainbow-oh/yee-c 0.4.5 → 0.5.0
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/dist/cjs/Dialog/demo/basic.js +1 -1
- package/dist/cjs/Dialog/dialog.js +1 -1
- package/dist/cjs/Field/demo/validation.js +23 -3
- package/dist/cjs/Field/field.js +1 -1
- package/dist/cjs/Form/Field.js +1 -4
- package/dist/cjs/Form/Form.js +1 -1
- package/dist/cjs/Form/Group.js +36 -14
- package/dist/cjs/Form/List.js +23 -3
- package/dist/cjs/Form/demo/async-validate.d.ts +3 -0
- package/dist/cjs/Form/demo/async-validate.js +108 -0
- package/dist/cjs/Form/demo/virtual.js +1 -1
- package/dist/cjs/Form/form-store.d.ts +4 -4
- package/dist/cjs/Form/form-store.js +55 -25
- package/dist/cjs/Form/interface.d.ts +10 -7
- package/dist/cjs/Form/utils/validate.d.ts +23 -2
- package/dist/cjs/Form/utils/validate.js +59 -10
- package/dist/cjs/Select/demo/basic.js +1 -1
- package/dist/cjs/Select/demo/virtual.d.ts +3 -0
- package/dist/cjs/Select/demo/virtual.js +55 -0
- package/dist/cjs/Select/hooks/useSelectKeyboard.d.ts +7 -1
- package/dist/cjs/Select/hooks/useSelectKeyboard.js +16 -6
- package/dist/cjs/Select/interface.d.ts +44 -0
- package/dist/cjs/Select/option.js +3 -2
- package/dist/cjs/Select/options.js +115 -15
- package/dist/cjs/Select/select.js +20 -4
- package/dist/cjs/Select/style/index.less +17 -0
- package/dist/cjs/Table/style/index.less +19 -0
- package/dist/cjs/hooks/index.d.ts +2 -1
- package/dist/cjs/hooks/index.js +3 -1
- package/dist/cjs/hooks/useVirtualList.d.ts +71 -0
- package/dist/cjs/hooks/useVirtualList.js +117 -0
- package/dist/cjs/style/themes.css +1 -1
- package/dist/cjs/style/variables.css +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/Dialog/demo/basic.js +1 -1
- package/dist/esm/Dialog/dialog.js +1 -1
- package/dist/esm/Field/demo/validation.js +30 -9
- package/dist/esm/Field/field.js +2 -1
- package/dist/esm/Form/Field.js +3 -4
- package/dist/esm/Form/Form.js +1 -1
- package/dist/esm/Form/Group.js +142 -53
- package/dist/esm/Form/List.js +61 -42
- package/dist/esm/Form/demo/async-validate.d.ts +3 -0
- package/dist/esm/Form/demo/async-validate.js +95 -0
- package/dist/esm/Form/demo/virtual.js +1 -1
- package/dist/esm/Form/form-store.d.ts +4 -4
- package/dist/esm/Form/form-store.js +187 -93
- package/dist/esm/Form/interface.d.ts +10 -7
- package/dist/esm/Form/utils/validate.d.ts +23 -2
- package/dist/esm/Form/utils/validate.js +122 -17
- package/dist/esm/Select/demo/basic.js +1 -1
- package/dist/esm/Select/demo/virtual.d.ts +3 -0
- package/dist/esm/Select/demo/virtual.js +38 -0
- package/dist/esm/Select/hooks/useSelectKeyboard.d.ts +7 -1
- package/dist/esm/Select/hooks/useSelectKeyboard.js +25 -9
- package/dist/esm/Select/interface.d.ts +44 -0
- package/dist/esm/Select/option.js +4 -2
- package/dist/esm/Select/options.js +132 -13
- package/dist/esm/Select/select.js +19 -4
- package/dist/esm/Select/style/index.less +17 -0
- package/dist/esm/Table/style/index.less +19 -0
- package/dist/esm/hooks/index.d.ts +2 -1
- package/dist/esm/hooks/index.js +2 -1
- package/dist/esm/hooks/useVirtualList.d.ts +71 -0
- package/dist/esm/hooks/useVirtualList.js +129 -0
- package/dist/esm/style/themes.css +1 -1
- package/dist/esm/style/variables.css +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/yee-c.min.css +1 -1
- package/dist/umd/yee-c.min.js +1 -1
- package/package.json +1 -1
- package/src/Dialog/demo/basic.tsx +1 -1
- package/src/Dialog/dialog.tsx +1 -1
- package/src/Field/demo/validation.tsx +2 -2
- package/src/Field/field.tsx +2 -1
- package/src/Field/index.en-US.md +1 -1
- package/src/Field/index.zh-CN.md +1 -1
- package/src/Form/Field.tsx +3 -4
- package/src/Form/Form.tsx +1 -1
- package/src/Form/Group.tsx +15 -12
- package/src/Form/List.tsx +3 -3
- package/src/Form/demo/async-validate.tsx +71 -0
- package/src/Form/demo/virtual.tsx +1 -1
- package/src/Form/form-store.ts +37 -21
- package/src/Form/index.en-US.md +6 -5
- package/src/Form/index.zh-CN.md +6 -5
- package/src/Form/interface.ts +13 -7
- package/src/Form/utils/validate.ts +65 -13
- package/src/Select/demo/basic.tsx +1 -1
- package/src/Select/demo/virtual.tsx +25 -0
- package/src/Select/hooks/useSelectKeyboard.ts +30 -6
- package/src/Select/index.en-US.md +4 -0
- package/src/Select/index.zh-CN.md +4 -0
- package/src/Select/interface.ts +48 -2
- package/src/Select/option.tsx +2 -1
- package/src/Select/options.tsx +139 -8
- package/src/Select/select.tsx +23 -1
- package/src/Select/style/index.less +17 -0
- package/src/Table/style/index.less +19 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useVirtualList.ts +169 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/Dialog/dialog.tsx
CHANGED
|
@@ -8,10 +8,10 @@ export default () => {
|
|
|
8
8
|
createForm('validationForm');
|
|
9
9
|
}, []);
|
|
10
10
|
|
|
11
|
-
const handleSubmit = () => {
|
|
11
|
+
const handleSubmit = async () => {
|
|
12
12
|
const form = getForm('validationForm');
|
|
13
13
|
console.log('form: ', form);
|
|
14
|
-
const errors = form?.submit();
|
|
14
|
+
const errors = await form?.submit();
|
|
15
15
|
console.log('errors: ', errors);
|
|
16
16
|
if (errors && errors.length === 0) {
|
|
17
17
|
console.log('Submit success:', form?.getFieldsValue());
|
package/src/Field/field.tsx
CHANGED
|
@@ -82,7 +82,8 @@ const Field: FC<FieldProps> = (props) => {
|
|
|
82
82
|
forceUpdate();
|
|
83
83
|
},
|
|
84
84
|
onBlur: (event: unknown) => {
|
|
85
|
-
|
|
85
|
+
// Fire-and-forget async validation; store notifies onStoreChange on completion.
|
|
86
|
+
void validateField?.(name, 'onBlur');
|
|
86
87
|
|
|
87
88
|
// Call child component's onBlur
|
|
88
89
|
if ((children.props as any).onBlur) {
|
package/src/Field/index.en-US.md
CHANGED
|
@@ -49,6 +49,6 @@ A standalone form field component that works without a `<Form>` wrapper. Uses `u
|
|
|
49
49
|
| minLength | `number` | Minimum length | - |
|
|
50
50
|
| maxLength | `number` | Maximum length | - |
|
|
51
51
|
| regexp | `RegExp` | Regex pattern | - |
|
|
52
|
-
| validator | `(value: unknown) => boolean
|
|
52
|
+
| validator | `(value: unknown) => boolean \| void \| Promise<boolean \| void>` | Custom validation function, supports sync and async (see Form.Rule) | - |
|
|
53
53
|
| message | `string` | Error message (required) | - |
|
|
54
54
|
| validateTrigger | `'onBlur' \| 'onChange' \| 'onSubmit' \| Array` | When to trigger validation | - |
|
package/src/Field/index.zh-CN.md
CHANGED
|
@@ -49,6 +49,6 @@ toc: 'content'
|
|
|
49
49
|
| minLength | `number` | 最小长度 | - |
|
|
50
50
|
| maxLength | `number` | 最大长度 | - |
|
|
51
51
|
| regexp | `RegExp` | 正则校验 | - |
|
|
52
|
-
| validator | `(value: unknown) => boolean
|
|
52
|
+
| validator | `(value: unknown) => boolean \| void \| Promise<boolean \| void>` | 自定义校验函数,支持同步与异步(详见 Form.Rule) | - |
|
|
53
53
|
| message | `string` | 错误提示信息(必填) | - |
|
|
54
54
|
| validateTrigger | `'onBlur' \| 'onChange' \| 'onSubmit' \| Array` | 校验触发时机 | - |
|
package/src/Form/Field.tsx
CHANGED
|
@@ -139,10 +139,9 @@ const Field: React.FC<FieldProps> = (props) => {
|
|
|
139
139
|
props?.onChange?.(_value, ...rest);
|
|
140
140
|
},
|
|
141
141
|
onBlur: (event: unknown) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
142
|
+
// Fire-and-forget; the store calls onStoreChange (→ forceUpdate) when
|
|
143
|
+
// the (possibly async) validation completes, so the error UI refreshes.
|
|
144
|
+
void validateField?.(normalizedName, 'onBlur');
|
|
146
145
|
props?.onBlur?.(event);
|
|
147
146
|
},
|
|
148
147
|
};
|
package/src/Form/Form.tsx
CHANGED
package/src/Form/Group.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import React, {
|
|
|
9
9
|
useRef,
|
|
10
10
|
} from 'react';
|
|
11
11
|
import Grid from '../Grid';
|
|
12
|
+
import { runValidator } from './utils/validate';
|
|
12
13
|
import FieldContext from './FieldContext';
|
|
13
14
|
import FormContext from './FormContext';
|
|
14
15
|
import type { FieldGroupProps, NamePath, ValidateMessage } from './interface';
|
|
@@ -38,7 +39,9 @@ const Group: React.FC<FieldGroupProps> = (props) => {
|
|
|
38
39
|
const [, forceValidate] = useReducer((x) => x + 1, 0); // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
39
40
|
|
|
40
41
|
// Group validation
|
|
41
|
-
const validate = (
|
|
42
|
+
const validate = async (
|
|
43
|
+
force: boolean,
|
|
44
|
+
): Promise<ValidateMessage[]> => {
|
|
42
45
|
if (!required && (!rules || rules.length === 0)) return [];
|
|
43
46
|
|
|
44
47
|
// Skip if not touched and not forced validation
|
|
@@ -66,21 +69,21 @@ const Group: React.FC<FieldGroupProps> = (props) => {
|
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
// Custom rules validation
|
|
72
|
+
// Custom rules validation (supports sync + async validators)
|
|
70
73
|
if (rules) {
|
|
71
|
-
|
|
74
|
+
for (const rule of rules) {
|
|
72
75
|
if (rule.validator) {
|
|
73
|
-
const
|
|
74
|
-
if (!passed) {
|
|
76
|
+
const result = await runValidator(rule.validator, values);
|
|
77
|
+
if (!result.passed) {
|
|
75
78
|
errors.push({
|
|
76
79
|
name: childNames,
|
|
77
80
|
status: 'error',
|
|
78
81
|
value: values,
|
|
79
|
-
message: rule.message
|
|
82
|
+
message: result.message ?? rule.message ?? '',
|
|
80
83
|
});
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
|
-
}
|
|
86
|
+
}
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
return errors;
|
|
@@ -92,8 +95,8 @@ const Group: React.FC<FieldGroupProps> = (props) => {
|
|
|
92
95
|
[],
|
|
93
96
|
);
|
|
94
97
|
|
|
95
|
-
const runValidation = () => {
|
|
96
|
-
const result = validate(false);
|
|
98
|
+
const runValidation = async () => {
|
|
99
|
+
const result = await validate(false);
|
|
97
100
|
setErrors(result);
|
|
98
101
|
};
|
|
99
102
|
|
|
@@ -102,7 +105,7 @@ const Group: React.FC<FieldGroupProps> = (props) => {
|
|
|
102
105
|
const unsubscribes = childNames.map((name) =>
|
|
103
106
|
formInstance.subscribe(name, () => {
|
|
104
107
|
touchedRef.current = true;
|
|
105
|
-
runValidation();
|
|
108
|
+
void runValidation();
|
|
106
109
|
}),
|
|
107
110
|
);
|
|
108
111
|
return () => unsubscribes.forEach((fn) => fn());
|
|
@@ -111,8 +114,8 @@ const Group: React.FC<FieldGroupProps> = (props) => {
|
|
|
111
114
|
// Register group validator to FormStore
|
|
112
115
|
useEffect(() => {
|
|
113
116
|
const unregister = formInstance.registerGroupEntity(groupId, {
|
|
114
|
-
validate: (force) => {
|
|
115
|
-
const result = validate(force);
|
|
117
|
+
validate: async (force) => {
|
|
118
|
+
const result = await validate(force);
|
|
116
119
|
setErrors(result);
|
|
117
120
|
forceUpdate();
|
|
118
121
|
return result;
|
package/src/Form/List.tsx
CHANGED
|
@@ -68,7 +68,7 @@ const List: React.FC<FormListProps> = ({
|
|
|
68
68
|
|
|
69
69
|
// List operation methods
|
|
70
70
|
const operations = useMemo(() => {
|
|
71
|
-
const add = (defaultValue?: any, insertIndex?: number) => {
|
|
71
|
+
const add = async (defaultValue?: any, insertIndex?: number) => {
|
|
72
72
|
const currentValue = getListValue();
|
|
73
73
|
const pathStr = prefixName.join('.');
|
|
74
74
|
|
|
@@ -83,8 +83,8 @@ const List: React.FC<FormListProps> = ({
|
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
// Execute validation
|
|
87
|
-
const errors = validateFields?.(fieldPaths);
|
|
86
|
+
// Execute validation (async — await before deciding whether to add)
|
|
87
|
+
const errors = await validateFields?.(fieldPaths);
|
|
88
88
|
|
|
89
89
|
// If errors exist, prevent adding and show errors
|
|
90
90
|
if (errors && errors.length > 0) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Button, Form, Grid, Input, Space } from '@rainbow-oh/yee-c';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
// Simulate an async uniqueness check against a backend API.
|
|
5
|
+
const TAKEN_USERNAMES = new Set(['admin', 'root', 'test']);
|
|
6
|
+
function checkUsernameUnique(username: string): Promise<boolean> {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
setTimeout(() => resolve(!TAKEN_USERNAMES.has(username)), 600);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default () => {
|
|
13
|
+
const [form] = Form.useForm();
|
|
14
|
+
|
|
15
|
+
const onFinish = (values: Record<string, unknown>) => {
|
|
16
|
+
console.log('Received values:', values);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const onFinishFailed = (errorInfo: unknown) => {
|
|
20
|
+
console.log('Failed:', errorInfo);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
|
|
25
|
+
<Grid>
|
|
26
|
+
<Form.Field
|
|
27
|
+
name="username"
|
|
28
|
+
label="Username"
|
|
29
|
+
rules={[
|
|
30
|
+
{ required: true, message: 'Please input your username!' },
|
|
31
|
+
{
|
|
32
|
+
// Async validator: resolve to pass, throw / reject to fail.
|
|
33
|
+
// The thrown Error's message overrides rule.message.
|
|
34
|
+
validator: async (value) => {
|
|
35
|
+
const isUnique = await checkUsernameUnique(String(value));
|
|
36
|
+
if (!isUnique) {
|
|
37
|
+
throw new Error('This username is already taken');
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
// Validate on blur so we don't hit the "API" on every keystroke.
|
|
41
|
+
validateTrigger: 'onBlur',
|
|
42
|
+
},
|
|
43
|
+
]}
|
|
44
|
+
>
|
|
45
|
+
<Input placeholder="Try admin / root / test (already taken)" />
|
|
46
|
+
</Form.Field>
|
|
47
|
+
|
|
48
|
+
<Form.Field
|
|
49
|
+
name="email"
|
|
50
|
+
label="Email"
|
|
51
|
+
rules={[
|
|
52
|
+
{ required: true, message: 'Please input your email!' },
|
|
53
|
+
{
|
|
54
|
+
regexp: /^[\w.+-]+@[\w-]+\.[\w.-]+$/,
|
|
55
|
+
message: 'Please enter a valid email!',
|
|
56
|
+
},
|
|
57
|
+
]}
|
|
58
|
+
>
|
|
59
|
+
<Input placeholder="Please input your email" />
|
|
60
|
+
</Form.Field>
|
|
61
|
+
</Grid>
|
|
62
|
+
|
|
63
|
+
<Space>
|
|
64
|
+
<Button type="primary" htmlType="submit">
|
|
65
|
+
Submit
|
|
66
|
+
</Button>
|
|
67
|
+
<Button htmlType="reset">Reset</Button>
|
|
68
|
+
</Space>
|
|
69
|
+
</Form>
|
|
70
|
+
);
|
|
71
|
+
};
|
package/src/Form/form-store.ts
CHANGED
|
@@ -145,13 +145,14 @@ export class FormStore {
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
// Validate on onChange and onBlur
|
|
148
|
+
// Validate on onChange and onBlur (async — fire-and-forget so setFieldsValue
|
|
149
|
+
// stays synchronous for its callers; results surface via onStoreChange).
|
|
149
150
|
if (
|
|
150
151
|
trigger === 'onChange' ||
|
|
151
152
|
trigger === 'onBlur' ||
|
|
152
153
|
trigger === 'onSubmit'
|
|
153
154
|
) {
|
|
154
|
-
this.validateFields(Object.keys(newStore), trigger);
|
|
155
|
+
void this.validateFields(Object.keys(newStore), trigger);
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
this.refreshField(newStore);
|
|
@@ -202,7 +203,10 @@ export class FormStore {
|
|
|
202
203
|
this.setFieldsValue(clearStore, 'clear');
|
|
203
204
|
};
|
|
204
205
|
|
|
205
|
-
validateField = (
|
|
206
|
+
validateField = async (
|
|
207
|
+
name: Name,
|
|
208
|
+
trigger?: TRIGGER,
|
|
209
|
+
): Promise<ValidateMessage[]> => {
|
|
206
210
|
const entity = this.fieldMap.get(name);
|
|
207
211
|
if (!entity) {
|
|
208
212
|
return [];
|
|
@@ -231,32 +235,40 @@ export class FormStore {
|
|
|
231
235
|
});
|
|
232
236
|
}
|
|
233
237
|
const value = this.getFieldValue(name);
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
if (name && !
|
|
238
|
+
for (const rule of mergedRules) {
|
|
239
|
+
const result = await validate(rule, value);
|
|
240
|
+
if (name && !result.passed) {
|
|
237
241
|
err.push({
|
|
242
|
+
...rule,
|
|
238
243
|
name: name,
|
|
239
244
|
status: 'error',
|
|
240
245
|
value,
|
|
241
|
-
|
|
246
|
+
message: result.message ?? rule.message,
|
|
242
247
|
} as ValidateMessage);
|
|
243
248
|
}
|
|
244
|
-
}
|
|
249
|
+
}
|
|
245
250
|
if (err.length > 0) {
|
|
246
251
|
this.validateMessages.set(name, err);
|
|
247
252
|
} else if (this.validateMessages.has(name)) {
|
|
248
253
|
this.validateMessages.delete(name);
|
|
249
254
|
}
|
|
255
|
+
// Notify the field to re-render so async validation results surface in the UI.
|
|
256
|
+
entity?.onStoreChange();
|
|
250
257
|
return err;
|
|
251
258
|
};
|
|
252
259
|
|
|
253
|
-
validateFields = (
|
|
260
|
+
validateFields = async (
|
|
254
261
|
names = Array.from(this.fieldMap.keys()),
|
|
255
262
|
trigger?: TRIGGER,
|
|
256
|
-
) => {
|
|
263
|
+
): Promise<ValidateMessage[]> => {
|
|
264
|
+
// Validate fields in parallel; Promise.all preserves input order so error
|
|
265
|
+
// ordering stays stable regardless of how long each async validator takes.
|
|
266
|
+
const results = await Promise.all(
|
|
267
|
+
names.map((name) => this.validateField(name, trigger)),
|
|
268
|
+
);
|
|
257
269
|
let err: ValidateMessage[] = [];
|
|
258
|
-
names.forEach((name) => {
|
|
259
|
-
const messages =
|
|
270
|
+
names.forEach((name, index) => {
|
|
271
|
+
const messages = results[index];
|
|
260
272
|
err = err.concat(messages);
|
|
261
273
|
if (messages.length > 0) {
|
|
262
274
|
this.validateMessages.set(name, messages);
|
|
@@ -316,12 +328,16 @@ export class FormStore {
|
|
|
316
328
|
};
|
|
317
329
|
};
|
|
318
330
|
|
|
319
|
-
validateGroups = (force = false): ValidateMessage[] => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
331
|
+
validateGroups = async (force = false): Promise<ValidateMessage[]> => {
|
|
332
|
+
const results = await Promise.all(
|
|
333
|
+
Array.from(this.groupEntities.values()).map((entity) =>
|
|
334
|
+
entity.validate(force),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
return results.reduce(
|
|
338
|
+
(acc, messages) => acc.concat(messages),
|
|
339
|
+
[] as ValidateMessage[],
|
|
340
|
+
);
|
|
325
341
|
};
|
|
326
342
|
|
|
327
343
|
setCallbacks = (callbacks: Callbacks) => {
|
|
@@ -515,10 +531,10 @@ export class FormStore {
|
|
|
515
531
|
};
|
|
516
532
|
};
|
|
517
533
|
|
|
518
|
-
submit = () => {
|
|
534
|
+
submit = async (): Promise<ValidateMessage[]> => {
|
|
519
535
|
const { onFinish, onFinishFailed } = this.callbacks;
|
|
520
|
-
const fieldErrors = this.validateFields();
|
|
521
|
-
const groupErrors = this.validateGroups(true);
|
|
536
|
+
const fieldErrors = await this.validateFields();
|
|
537
|
+
const groupErrors = await this.validateGroups(true);
|
|
522
538
|
const allErrors = [...fieldErrors, ...groupErrors];
|
|
523
539
|
if (allErrors.length === 0) {
|
|
524
540
|
onFinish?.(this.getFieldsValue());
|
package/src/Form/index.en-US.md
CHANGED
|
@@ -20,6 +20,7 @@ High performance form component with data scope management.
|
|
|
20
20
|
<code src="./demo/watch.tsx" title="Field Watch" description="useWatch monitors field value changes for conditional rendering"></code>
|
|
21
21
|
<code src="./demo/layout.tsx" title="Layout" description="Different form layouts"></code>
|
|
22
22
|
<code src="./demo/validate.tsx" title="Validation" description="Form validation"></code>
|
|
23
|
+
<code src="./demo/async-validate.tsx" title="Async Validation" description="async validator for server-side checks like uniqueness"></code>
|
|
23
24
|
<code src="./demo/list.tsx" title="Form List" description="Form list field usage"></code>
|
|
24
25
|
<code src="./demo/form-table.tsx" title="Form Table" description="Form table field usage"></code>
|
|
25
26
|
<code src="./demo/dynamic.tsx" title="Dynamic Form" description="Dynamic form items"></code>
|
|
@@ -50,16 +51,16 @@ High performance form component with data scope management.
|
|
|
50
51
|
|
|
51
52
|
| Property | Type | Description |
|
|
52
53
|
| --------------------- | ------------------------------------------------------------------------------------------ | ------------------------- |
|
|
53
|
-
| getFieldValidate | `(name: Name) => ValidateMessage \| null`
|
|
54
|
+
| getFieldValidate | `(name: Name) => ValidateMessage \| null` | Get field validation (may lag before async validation completes; updates via onStoreChange) |
|
|
54
55
|
| getFieldValue | `(name: Name) => StoreValue` | Get field value |
|
|
55
|
-
| submit | `() =>
|
|
56
|
+
| submit | `() => Promise<ValidateMessage[]>` | Submit form (async, triggers onFinish only after validation completes) |
|
|
56
57
|
| getFieldsValue | `() => Values` | Get all field values |
|
|
57
58
|
| setFieldsValue | `(newStore: Store, trigger?: TRIGGER) => void` | Set field values |
|
|
58
59
|
| setCallbacks | `(callbacks: Callbacks) => void` | Set callbacks |
|
|
59
60
|
| resetFields | `(name?: Name[]) => void` | Reset fields |
|
|
60
61
|
| clearFields | `(name?: Name[]) => void` | Clear fields |
|
|
61
|
-
| validateField | `(name: Name, trigger?: 'onChange' \| 'onBlur') => ValidateMessage[]
|
|
62
|
-
| validateFields | `(names?: Name[], trigger?: TRIGGER) => ValidateMessage[]
|
|
62
|
+
| validateField | `(name: Name, trigger?: 'onChange' \| 'onBlur') => Promise<ValidateMessage[]>` | Validate single field |
|
|
63
|
+
| validateFields | `(names?: Name[], trigger?: TRIGGER) => Promise<ValidateMessage[]>` | Validate multiple fields |
|
|
63
64
|
| getCallbacks | `() => Callbacks` | Get callbacks |
|
|
64
65
|
| registerFieldEntities | `(entity: FieldEntity) => void` | Register field entities |
|
|
65
66
|
| initialize | `({ initialValues, callbacks }: { initialValues?: Store; callbacks?: Callbacks }) => void` | Initialize form |
|
|
@@ -117,7 +118,7 @@ High performance form component with data scope management.
|
|
|
117
118
|
| maxLength | `number` | Maximum length |
|
|
118
119
|
| regexp | `RegExp` | Regular expression |
|
|
119
120
|
| message | `string` | Error message |
|
|
120
|
-
| validator | `(value: unknown) => boolean
|
|
121
|
+
| validator | `(value: unknown) => boolean \| void \| Promise<boolean \| void>` | Custom validator. Supports sync and async: sync `return false`/`throw` fails; async `resolve(false)`/`reject`/`throw` fails. When throwing/rejecting with an Error, `error.message` overrides `message` |
|
|
121
122
|
| validateTrigger | `'onBlur' \| 'onChange' \| 'onSubmit' \| Array<'onBlur' \| 'onChange' \| 'onSubmit'>` | Validation trigger |
|
|
122
123
|
|
|
123
124
|
### Types
|
package/src/Form/index.zh-CN.md
CHANGED
|
@@ -22,6 +22,7 @@ toc: 'content'
|
|
|
22
22
|
<code src="./demo/form-table.tsx" title="表单列表" description="Form的列表字段用法"></code>
|
|
23
23
|
<code src="./demo/layout.tsx" title="表单布局" description="不同的表单布局"></code>
|
|
24
24
|
<code src="./demo/validate.tsx" title="表单校验" description="表单校验功能"></code>
|
|
25
|
+
<code src="./demo/async-validate.tsx" title="异步校验" description="async validator 实现服务端唯一性校验等异步场景"></code>
|
|
25
26
|
|
|
26
27
|
## API
|
|
27
28
|
|
|
@@ -49,16 +50,16 @@ toc: 'content'
|
|
|
49
50
|
|
|
50
51
|
| 属性名 | 类型 | 描述 |
|
|
51
52
|
| --------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------ |
|
|
52
|
-
| getFieldValidate | `(name: Name) => ValidateMessage \| null`
|
|
53
|
+
| getFieldValidate | `(name: Name) => ValidateMessage \| null` | 获取字段验证(异步校验未完成时可能读到旧值,最终会随 onStoreChange 更新) |
|
|
53
54
|
| getFieldValue | `(name: Name) => StoreValue` | 获取字段值 |
|
|
54
|
-
| submit | `() =>
|
|
55
|
+
| submit | `() => Promise<ValidateMessage[]>` | 提交表单(异步,校验完成后才触发 onFinish) |
|
|
55
56
|
| getFieldsValue | `() => Values` | 获取所有字段值 |
|
|
56
57
|
| setFieldsValue | `(newStore: Store, trigger?: TRIGGER) => void` | 设置字段值 |
|
|
57
58
|
| setCallbacks | `(callbacks: Callbacks) => void` | 设置回调 |
|
|
58
59
|
| resetFields | `(name?: Name[]) => void` | 重置字段 |
|
|
59
60
|
| clearFields | `(name?: Name[]) => void` | 清空字段 |
|
|
60
|
-
| validateField | `(name: Name, trigger?: 'onChange' \| 'onBlur') => ValidateMessage[]
|
|
61
|
-
| validateFields | `(names?: Name[], trigger?: TRIGGER) => ValidateMessage[]
|
|
61
|
+
| validateField | `(name: Name, trigger?: 'onChange' \| 'onBlur') => Promise<ValidateMessage[]>` | 校验单个字段 |
|
|
62
|
+
| validateFields | `(names?: Name[], trigger?: TRIGGER) => Promise<ValidateMessage[]>` | 校验多个字段 |
|
|
62
63
|
| getCallbacks | `() => Callbacks` | 获取回调 |
|
|
63
64
|
| registerFieldEntities | `(entity: FieldEntity) => void` | 注册字段实体 |
|
|
64
65
|
| initialize | `({ initialValues, callbacks }: { initialValues?: Store; callbacks?: Callbacks }) => void` | 初始化表单 |
|
|
@@ -116,7 +117,7 @@ toc: 'content'
|
|
|
116
117
|
| maxLength | `number` | 最大长度 |
|
|
117
118
|
| regexp | `RegExp` | 正则表达式 |
|
|
118
119
|
| message | `string` | 错误信息 |
|
|
119
|
-
| validator | `(value: unknown) => boolean
|
|
120
|
+
| validator | `(value: unknown) => boolean \| void \| Promise<boolean \| void>` | 自定义校验函数,支持同步与异步。同步返回 `false` 或 `throw` 表示失败;异步 `resolve(false)`/`reject`/`throw` 表示失败。`throw`/`reject` 时 `error.message` 覆盖 `message` |
|
|
120
121
|
| validateTrigger | `'onBlur' \| 'onChange' \| 'onSubmit' \| Array<'onBlur' \| 'onChange' \| 'onSubmit'>` | 校验触发时机 |
|
|
121
122
|
|
|
122
123
|
### Types
|
package/src/Form/interface.ts
CHANGED
|
@@ -113,7 +113,7 @@ export interface FormInstance<Values = any> {
|
|
|
113
113
|
/**
|
|
114
114
|
* Submit form
|
|
115
115
|
*/
|
|
116
|
-
submit: () => ValidateMessage[]
|
|
116
|
+
submit: () => Promise<ValidateMessage[]>;
|
|
117
117
|
/**
|
|
118
118
|
* Get all field values
|
|
119
119
|
*/
|
|
@@ -140,7 +140,7 @@ export interface FormInstance<Values = any> {
|
|
|
140
140
|
validateField: (
|
|
141
141
|
name: Name,
|
|
142
142
|
trigger?: 'onChange' | 'onBlur',
|
|
143
|
-
) => ValidateMessage[]
|
|
143
|
+
) => Promise<ValidateMessage[]>;
|
|
144
144
|
/**
|
|
145
145
|
* Get callback functions
|
|
146
146
|
*/
|
|
@@ -152,7 +152,10 @@ export interface FormInstance<Values = any> {
|
|
|
152
152
|
/**
|
|
153
153
|
* Validate fields and get error messages
|
|
154
154
|
*/
|
|
155
|
-
validateFields: (
|
|
155
|
+
validateFields: (
|
|
156
|
+
names?: Name[],
|
|
157
|
+
trigger?: TRIGGER,
|
|
158
|
+
) => Promise<ValidateMessage[]>;
|
|
156
159
|
/**
|
|
157
160
|
* Initialize form - mainly used to set initial values and callbacks
|
|
158
161
|
*/
|
|
@@ -233,9 +236,12 @@ export type Rule = {
|
|
|
233
236
|
*/
|
|
234
237
|
message?: string;
|
|
235
238
|
/**
|
|
236
|
-
* Custom validation function
|
|
239
|
+
* Custom validation function. Supports sync and async:
|
|
240
|
+
* - sync: return true/undefined to pass, return false or throw to fail
|
|
241
|
+
* - async: return a Promise (resolve to pass, reject/throw to fail)
|
|
242
|
+
* When throwing/rejecting with an Error, its message overrides rule.message.
|
|
237
243
|
*/
|
|
238
|
-
validator?: (value: unknown) => boolean
|
|
244
|
+
validator?: (value: unknown) => boolean | void | Promise<boolean | void>;
|
|
239
245
|
/**
|
|
240
246
|
* Validation trigger timing
|
|
241
247
|
*/
|
|
@@ -354,7 +360,7 @@ export type FormListField<T = any> = {
|
|
|
354
360
|
};
|
|
355
361
|
|
|
356
362
|
export type FormListOperation = {
|
|
357
|
-
add: (defaultValue?: any, insertIndex?: number) => void
|
|
363
|
+
add: (defaultValue?: any, insertIndex?: number) => void | Promise<void>;
|
|
358
364
|
remove: (index: number | number[]) => void;
|
|
359
365
|
move: (from: number, to: number) => void;
|
|
360
366
|
};
|
|
@@ -403,6 +409,6 @@ export type FieldGroupProps = {
|
|
|
403
409
|
};
|
|
404
410
|
|
|
405
411
|
export interface GroupEntity {
|
|
406
|
-
validate: (force: boolean) => ValidateMessage[]
|
|
412
|
+
validate: (force: boolean) => Promise<ValidateMessage[]>;
|
|
407
413
|
reset: () => void;
|
|
408
414
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { Rule } from '../interface';
|
|
1
|
+
import type { Rule } from '../interface';
|
|
3
2
|
|
|
4
3
|
function isEmpty(v: unknown) {
|
|
5
4
|
return v === undefined || v === null || v === '' ? true : false;
|
|
@@ -60,11 +59,15 @@ function verifyRuleOfRegexp(regexp: RegExp, value: unknown) {
|
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
export interface ValidateResult {
|
|
63
|
+
passed: boolean;
|
|
64
|
+
// Custom message from a thrown/rejected validator; falls back to rule.message upstream
|
|
65
|
+
message?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check the synchronous built-in rules (min/max/minLength/maxLength/regexp).
|
|
69
|
+
function verifySyncRules(rule: Rule, value: unknown): boolean {
|
|
70
|
+
const { min, max, minLength, maxLength, regexp } = rule;
|
|
68
71
|
|
|
69
72
|
if (typeof min !== 'undefined' && !verifyRuleOfMin(min, value)) {
|
|
70
73
|
return false;
|
|
@@ -87,18 +90,67 @@ function verifyMain(rule: Rule, value: any): boolean {
|
|
|
87
90
|
if (regexp instanceof RegExp && !verifyRuleOfRegexp(regexp, value)) {
|
|
88
91
|
return false;
|
|
89
92
|
}
|
|
90
|
-
if (typeof validator === 'function' && !validator(value)) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
93
|
return true;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Run a custom validator, dispatching between sync and async at runtime.
|
|
98
|
+
*
|
|
99
|
+
* Supports all of the following shapes (backwards compatible):
|
|
100
|
+
* - sync `return true` / `return undefined` → pass
|
|
101
|
+
* - sync `return false` → fail (use rule.message)
|
|
102
|
+
* - sync `throw new Error(msg)` → fail (use msg)
|
|
103
|
+
* - async `return Promise.resolve()` → pass
|
|
104
|
+
* - async `return Promise.resolve(false)` → fail (use rule.message)
|
|
105
|
+
* - async `throw` / `return Promise.reject(new Error(msg))` → fail (use msg)
|
|
106
|
+
*/
|
|
107
|
+
export async function runValidator(
|
|
108
|
+
validator: NonNullable<Rule['validator']>,
|
|
109
|
+
value: unknown,
|
|
110
|
+
): Promise<ValidateResult> {
|
|
111
|
+
try {
|
|
112
|
+
const res = validator(value);
|
|
113
|
+
if (res instanceof Promise) {
|
|
114
|
+
const resolved = await res;
|
|
115
|
+
if (resolved === false) {
|
|
116
|
+
return { passed: false };
|
|
117
|
+
}
|
|
118
|
+
return { passed: true };
|
|
119
|
+
}
|
|
120
|
+
if (res === false) {
|
|
121
|
+
return { passed: false };
|
|
122
|
+
}
|
|
123
|
+
return { passed: true };
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return {
|
|
126
|
+
passed: false,
|
|
127
|
+
message: e instanceof Error ? e.message : String(e),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validate a single rule against a value.
|
|
134
|
+
* Returns { passed, message? }. Sync rules short-circuit; the custom validator
|
|
135
|
+
* is awaited so async validators are supported.
|
|
136
|
+
*/
|
|
137
|
+
async function validate(
|
|
138
|
+
rule: Rule,
|
|
139
|
+
value: unknown,
|
|
140
|
+
): Promise<ValidateResult> {
|
|
97
141
|
if (rule.required && isEmpty(value)) {
|
|
98
|
-
return false;
|
|
142
|
+
return { passed: false };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!verifySyncRules(rule, value)) {
|
|
146
|
+
return { passed: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof rule.validator === 'function') {
|
|
150
|
+
return runValidator(rule.validator, value);
|
|
99
151
|
}
|
|
100
152
|
|
|
101
|
-
return
|
|
153
|
+
return { passed: true };
|
|
102
154
|
}
|
|
103
155
|
|
|
104
156
|
export default validate;
|