@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.
Files changed (104) hide show
  1. package/dist/cjs/Dialog/demo/basic.js +1 -1
  2. package/dist/cjs/Dialog/dialog.js +1 -1
  3. package/dist/cjs/Field/demo/validation.js +23 -3
  4. package/dist/cjs/Field/field.js +1 -1
  5. package/dist/cjs/Form/Field.js +1 -4
  6. package/dist/cjs/Form/Form.js +1 -1
  7. package/dist/cjs/Form/Group.js +36 -14
  8. package/dist/cjs/Form/List.js +23 -3
  9. package/dist/cjs/Form/demo/async-validate.d.ts +3 -0
  10. package/dist/cjs/Form/demo/async-validate.js +108 -0
  11. package/dist/cjs/Form/demo/virtual.js +1 -1
  12. package/dist/cjs/Form/form-store.d.ts +4 -4
  13. package/dist/cjs/Form/form-store.js +55 -25
  14. package/dist/cjs/Form/interface.d.ts +10 -7
  15. package/dist/cjs/Form/utils/validate.d.ts +23 -2
  16. package/dist/cjs/Form/utils/validate.js +59 -10
  17. package/dist/cjs/Select/demo/basic.js +1 -1
  18. package/dist/cjs/Select/demo/virtual.d.ts +3 -0
  19. package/dist/cjs/Select/demo/virtual.js +55 -0
  20. package/dist/cjs/Select/hooks/useSelectKeyboard.d.ts +7 -1
  21. package/dist/cjs/Select/hooks/useSelectKeyboard.js +16 -6
  22. package/dist/cjs/Select/interface.d.ts +44 -0
  23. package/dist/cjs/Select/option.js +3 -2
  24. package/dist/cjs/Select/options.js +115 -15
  25. package/dist/cjs/Select/select.js +20 -4
  26. package/dist/cjs/Select/style/index.less +17 -0
  27. package/dist/cjs/Table/style/index.less +19 -0
  28. package/dist/cjs/hooks/index.d.ts +2 -1
  29. package/dist/cjs/hooks/index.js +3 -1
  30. package/dist/cjs/hooks/useVirtualList.d.ts +71 -0
  31. package/dist/cjs/hooks/useVirtualList.js +117 -0
  32. package/dist/cjs/style/themes.css +1 -1
  33. package/dist/cjs/style/variables.css +1 -1
  34. package/dist/cjs/version.d.ts +1 -1
  35. package/dist/cjs/version.js +1 -1
  36. package/dist/esm/Dialog/demo/basic.js +1 -1
  37. package/dist/esm/Dialog/dialog.js +1 -1
  38. package/dist/esm/Field/demo/validation.js +30 -9
  39. package/dist/esm/Field/field.js +2 -1
  40. package/dist/esm/Form/Field.js +3 -4
  41. package/dist/esm/Form/Form.js +1 -1
  42. package/dist/esm/Form/Group.js +142 -53
  43. package/dist/esm/Form/List.js +61 -42
  44. package/dist/esm/Form/demo/async-validate.d.ts +3 -0
  45. package/dist/esm/Form/demo/async-validate.js +95 -0
  46. package/dist/esm/Form/demo/virtual.js +1 -1
  47. package/dist/esm/Form/form-store.d.ts +4 -4
  48. package/dist/esm/Form/form-store.js +187 -93
  49. package/dist/esm/Form/interface.d.ts +10 -7
  50. package/dist/esm/Form/utils/validate.d.ts +23 -2
  51. package/dist/esm/Form/utils/validate.js +122 -17
  52. package/dist/esm/Select/demo/basic.js +1 -1
  53. package/dist/esm/Select/demo/virtual.d.ts +3 -0
  54. package/dist/esm/Select/demo/virtual.js +38 -0
  55. package/dist/esm/Select/hooks/useSelectKeyboard.d.ts +7 -1
  56. package/dist/esm/Select/hooks/useSelectKeyboard.js +25 -9
  57. package/dist/esm/Select/interface.d.ts +44 -0
  58. package/dist/esm/Select/option.js +4 -2
  59. package/dist/esm/Select/options.js +132 -13
  60. package/dist/esm/Select/select.js +19 -4
  61. package/dist/esm/Select/style/index.less +17 -0
  62. package/dist/esm/Table/style/index.less +19 -0
  63. package/dist/esm/hooks/index.d.ts +2 -1
  64. package/dist/esm/hooks/index.js +2 -1
  65. package/dist/esm/hooks/useVirtualList.d.ts +71 -0
  66. package/dist/esm/hooks/useVirtualList.js +129 -0
  67. package/dist/esm/style/themes.css +1 -1
  68. package/dist/esm/style/variables.css +1 -1
  69. package/dist/esm/version.d.ts +1 -1
  70. package/dist/esm/version.js +1 -1
  71. package/dist/umd/yee-c.min.css +1 -1
  72. package/dist/umd/yee-c.min.js +1 -1
  73. package/package.json +1 -1
  74. package/src/Dialog/demo/basic.tsx +1 -1
  75. package/src/Dialog/dialog.tsx +1 -1
  76. package/src/Field/demo/validation.tsx +2 -2
  77. package/src/Field/field.tsx +2 -1
  78. package/src/Field/index.en-US.md +1 -1
  79. package/src/Field/index.zh-CN.md +1 -1
  80. package/src/Form/Field.tsx +3 -4
  81. package/src/Form/Form.tsx +1 -1
  82. package/src/Form/Group.tsx +15 -12
  83. package/src/Form/List.tsx +3 -3
  84. package/src/Form/demo/async-validate.tsx +71 -0
  85. package/src/Form/demo/virtual.tsx +1 -1
  86. package/src/Form/form-store.ts +37 -21
  87. package/src/Form/index.en-US.md +6 -5
  88. package/src/Form/index.zh-CN.md +6 -5
  89. package/src/Form/interface.ts +13 -7
  90. package/src/Form/utils/validate.ts +65 -13
  91. package/src/Select/demo/basic.tsx +1 -1
  92. package/src/Select/demo/virtual.tsx +25 -0
  93. package/src/Select/hooks/useSelectKeyboard.ts +30 -6
  94. package/src/Select/index.en-US.md +4 -0
  95. package/src/Select/index.zh-CN.md +4 -0
  96. package/src/Select/interface.ts +48 -2
  97. package/src/Select/option.tsx +2 -1
  98. package/src/Select/options.tsx +139 -8
  99. package/src/Select/select.tsx +23 -1
  100. package/src/Select/style/index.less +17 -0
  101. package/src/Table/style/index.less +19 -0
  102. package/src/hooks/index.ts +2 -0
  103. package/src/hooks/useVirtualList.ts +169 -0
  104. package/src/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-oh/yee-c",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "yee basic components",
5
5
  "keywords": [
6
6
  "react",
@@ -22,7 +22,7 @@ export default () => {
22
22
  setOpen(false);
23
23
  // form.resetFields();
24
24
  }}
25
- onConfirm={() => form.submit()}
25
+ onConfirm={() => void form.submit()}
26
26
  >
27
27
  <Form
28
28
  form={form}
@@ -90,7 +90,7 @@ const Dialog = (baseprops: DialogProps) => {
90
90
  });
91
91
 
92
92
  const renderFooter = () => {
93
- if (footer === false) {
93
+ if (footer === false || footer === null) {
94
94
  return null;
95
95
  }
96
96
 
@@ -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());
@@ -82,7 +82,8 @@ const Field: FC<FieldProps> = (props) => {
82
82
  forceUpdate();
83
83
  },
84
84
  onBlur: (event: unknown) => {
85
- validateField?.(name, 'onBlur');
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) {
@@ -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` | Custom validation function | - |
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 | - |
@@ -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` | 校验触发时机 | - |
@@ -139,10 +139,9 @@ const Field: React.FC<FieldProps> = (props) => {
139
139
  props?.onChange?.(_value, ...rest);
140
140
  },
141
141
  onBlur: (event: unknown) => {
142
- const err = validateField?.(normalizedName, 'onBlur');
143
- if (err.length) {
144
- forceUpdate();
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
@@ -60,7 +60,7 @@ const Form: React.FC<FormProps> = (baseprops) => {
60
60
  onSubmit={(e) => {
61
61
  e.preventDefault();
62
62
  e.stopPropagation();
63
- formInstance.submit();
63
+ void formInstance.submit();
64
64
  }}
65
65
  onReset={(e) => {
66
66
  e.preventDefault();
@@ -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 = (force: boolean): ValidateMessage[] => {
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
- rules.forEach((rule) => {
74
+ for (const rule of rules) {
72
75
  if (rule.validator) {
73
- const passed = rule.validator(values);
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
+ };
@@ -50,7 +50,7 @@ export default function Virtual() {
50
50
  type="primary"
51
51
  onClick={() => {
52
52
  const form = getForm('form');
53
- form.submit();
53
+ void form.submit();
54
54
  }}
55
55
  >
56
56
  Submit
@@ -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 = (name: Name, trigger?: TRIGGER) => {
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
- mergedRules.forEach((rule) => {
235
- const mark = validate(rule, value);
236
- if (name && !mark) {
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
- ...rule,
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 = this.validateField(name, trigger);
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
- let err: ValidateMessage[] = [];
321
- this.groupEntities.forEach((entity) => {
322
- err = err.concat(entity.validate(force));
323
- });
324
- return err;
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());
@@ -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` | Get field validation |
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 | `() => void` | Submit form |
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[]` | Validate single field |
62
- | validateFields | `(names?: Name[], trigger?: TRIGGER) => ValidateMessage[]` | Validate multiple fields |
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` | Custom validator function |
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
@@ -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 | `() => void` | 提交表单 |
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
@@ -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: (names?: Name[], trigger?: TRIGGER) => ValidateMessage[];
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, returns true for pass, false or throw for fail
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
- // true: 校验通过;false校验未通过
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
- * @returns validated
65
- * */
66
- function verifyMain(rule: Rule, value: any): boolean {
67
- const { min, max, minLength, maxLength, regexp, validator } = rule;
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
- function validate(rule: Rule, value: unknown): boolean {
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 verifyMain(rule, value);
153
+ return { passed: true };
102
154
  }
103
155
 
104
156
  export default validate;
@@ -15,7 +15,7 @@ const options = [
15
15
  ];
16
16
 
17
17
  export default () => {
18
- const [value, setValue] = useState('apple');
18
+ const [value, setValue] = useState('mango');
19
19
 
20
20
  return (
21
21
  <Select