@rainbow-oh/yee-c 0.8.20 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-oh/yee-c",
3
- "version": "0.8.20",
3
+ "version": "0.9.0",
4
4
  "description": "yee basic components",
5
5
  "keywords": [
6
6
  "react",
@@ -44,7 +44,7 @@ const Field: React.FC<FieldProps> = (props) => {
44
44
  } = props;
45
45
  const {
46
46
  getCallbacks,
47
- getFieldValidate,
47
+ getFieldStatus,
48
48
  getFieldValue,
49
49
  getFieldsValue,
50
50
  setFieldsValue,
@@ -88,7 +88,7 @@ const Field: React.FC<FieldProps> = (props) => {
88
88
  );
89
89
  };
90
90
 
91
- const validate = getFieldValidate?.(normalizedName);
91
+ const fieldStatus = getFieldStatus?.(normalizedName);
92
92
 
93
93
  // Normalize tooltip into { icon, ...tooltipProps }, or null when not provided.
94
94
  // Everything except `icon` is forwarded to <Tooltip>, so any Tooltip prop
@@ -152,8 +152,7 @@ const Field: React.FC<FieldProps> = (props) => {
152
152
  `${prefixCls}-item`,
153
153
  `${prefixCls}-item-${layout}`,
154
154
  {
155
- [`${prefixCls}-item-${validate?.status}`]:
156
- validate && validate.status,
155
+ [`${prefixCls}-item-${fieldStatus?.status}`]: fieldStatus?.status,
157
156
  },
158
157
  className,
159
158
  )}
@@ -200,18 +199,18 @@ const Field: React.FC<FieldProps> = (props) => {
200
199
  </div>
201
200
  </div>
202
201
  <AnimatePresence>
203
- {validate && (
202
+ {fieldStatus?.message && (
204
203
  <motion.small
205
204
  initial={{ opacity: 0, y: -10 }}
206
205
  animate={{ opacity: 1, y: 0 }}
207
206
  exit={{ opacity: 0, y: -10 }}
208
207
  transition={{ duration: 0.16 }}
209
208
  className={clsx(`${prefixCls}-item-additional`, {
210
- [`${prefixCls}-item-additional-${validate.status}`]:
211
- validate.status,
209
+ [`${prefixCls}-item-additional-${fieldStatus.status}`]:
210
+ fieldStatus.status,
212
211
  })}
213
212
  >
214
- {validate.message}
213
+ {fieldStatus.message}
215
214
  </motion.small>
216
215
  )}
217
216
  </AnimatePresence>
@@ -0,0 +1,108 @@
1
+ import {
2
+ Button,
3
+ Form,
4
+ Input,
5
+ Space,
6
+ type FieldStatusData,
7
+ } from '@rainbow-oh/yee-c';
8
+ import React, { useState } from 'react';
9
+
10
+ export default () => {
11
+ const [form] = Form.useForm();
12
+ const [currentStatus, setCurrentStatus] = useState<FieldStatusData | null>(
13
+ null,
14
+ );
15
+ const [submitResult, setSubmitResult] = useState('');
16
+
17
+ const applyStatus = (status: FieldStatusData | null) => {
18
+ form.setFieldStatus('username', status);
19
+ setCurrentStatus(form.getFieldStatus('username'));
20
+ setSubmitResult('');
21
+ };
22
+
23
+ return (
24
+ <Form
25
+ form={form}
26
+ initialValues={{ username: 'demo-user' }}
27
+ onFinish={() => setSubmitResult('Submit succeeded')}
28
+ onFinishFailed={() =>
29
+ setSubmitResult('Submit blocked by the blocking error')
30
+ }
31
+ >
32
+ <Form.Field
33
+ name="username"
34
+ label="Username"
35
+ rules={[{ required: true, message: 'Please input your username!' }]}
36
+ >
37
+ <Input placeholder="Please input your username" />
38
+ </Form.Field>
39
+
40
+ <Space style={{ marginTop: 28, flexWrap: 'wrap' }}>
41
+ <Button
42
+ onClick={() =>
43
+ applyStatus({
44
+ status: 'error',
45
+ message: 'This username is unavailable',
46
+ })
47
+ }
48
+ >
49
+ Error
50
+ </Button>
51
+ <Button
52
+ onClick={() =>
53
+ applyStatus({
54
+ status: 'error',
55
+ message: 'This error prevents submission',
56
+ blocking: true,
57
+ })
58
+ }
59
+ >
60
+ Blocking error
61
+ </Button>
62
+ <Button
63
+ onClick={() =>
64
+ applyStatus({
65
+ status: 'warning',
66
+ message: 'This username is commonly used',
67
+ })
68
+ }
69
+ >
70
+ Warning
71
+ </Button>
72
+ <Button
73
+ onClick={() =>
74
+ applyStatus({
75
+ status: 'success',
76
+ message: 'This username is available',
77
+ })
78
+ }
79
+ >
80
+ Success
81
+ </Button>
82
+ <Button
83
+ onClick={() =>
84
+ applyStatus({
85
+ status: 'info',
86
+ message: 'The username can be changed later',
87
+ })
88
+ }
89
+ >
90
+ Info
91
+ </Button>
92
+ <Button onClick={() => applyStatus(null)}>Clear</Button>
93
+ <Button type="primary" htmlType="submit">
94
+ Submit
95
+ </Button>
96
+ </Space>
97
+
98
+ <pre style={{ marginTop: 16 }}>
99
+ Current status: {JSON.stringify(currentStatus, null, 2)}
100
+ </pre>
101
+ {submitResult ? <div>{submitResult}</div> : null}
102
+ <div style={{ marginTop: 8, color: '#999' }}>
103
+ Tip: set Success, then clear the input — the rule error takes display
104
+ precedence over the manually set status.
105
+ </div>
106
+ </Form>
107
+ );
108
+ };
@@ -9,6 +9,7 @@ import validate from './utils/validate';
9
9
  import type {
10
10
  Callbacks,
11
11
  FieldEntity,
12
+ FieldStatusData,
12
13
  FormInstance,
13
14
  FormListField,
14
15
  FormListOperation,
@@ -43,6 +44,7 @@ export class FormStore {
43
44
  private fieldEntities: FieldEntity[] = [];
44
45
  private fieldMap: Map<Name, FieldEntity> = new Map();
45
46
  private validateMessages: Map<Name, ValidateMessage[]> = new Map();
47
+ private fieldStatuses: Map<Name, FieldStatusData> = new Map();
46
48
  private initialized = false;
47
49
  private initialValues: Store = {};
48
50
  // List key manager
@@ -114,6 +116,76 @@ export class FormStore {
114
116
  return null;
115
117
  };
116
118
 
119
+ getFieldStatus = (name: NamePath): FieldStatusData | null => {
120
+ const normalizedName = stringifyPath(getNamePath(name));
121
+ const manualStatus = this.fieldStatuses.get(normalizedName);
122
+ const validateMessage = this.validateMessages.get(normalizedName)?.[0];
123
+
124
+ // Rule errors reflect the current value and auto-clear once validation
125
+ // passes, so they always take display precedence — a stale manually set
126
+ // status must never hide why submission failed.
127
+ if (validateMessage?.status === 'error') {
128
+ return {
129
+ status: 'error',
130
+ message: validateMessage.message,
131
+ };
132
+ }
133
+
134
+ if (manualStatus) {
135
+ return { ...manualStatus };
136
+ }
137
+
138
+ if (validateMessage) {
139
+ return {
140
+ status: validateMessage.status,
141
+ message: validateMessage.message,
142
+ };
143
+ }
144
+
145
+ return null;
146
+ };
147
+
148
+ setFieldStatus = (name: NamePath, status: FieldStatusData | null) => {
149
+ const normalizedName = stringifyPath(getNamePath(name));
150
+ if (!normalizedName) return;
151
+
152
+ if (status) {
153
+ this.fieldStatuses.set(normalizedName, { ...status });
154
+ } else {
155
+ this.fieldStatuses.delete(normalizedName);
156
+ }
157
+
158
+ this.fieldMap.get(normalizedName)?.onStoreChange();
159
+ };
160
+
161
+ // Delete entries whose key equals or nests under one of `names`
162
+ // (no names = delete all). Keeps rule messages and manual statuses in
163
+ // sync with the fields that reset/clear actually touched.
164
+ private deleteRelatedEntries = <T>(
165
+ entries: Map<Name, T>,
166
+ names?: NamePath[],
167
+ ): void => {
168
+ if (!names) {
169
+ entries.clear();
170
+ return;
171
+ }
172
+
173
+ const normalizedNames = names.map((name) =>
174
+ stringifyPath(getNamePath(name)),
175
+ );
176
+ Array.from(entries.keys()).forEach((fieldName) => {
177
+ if (
178
+ normalizedNames.some((name) => isRelatedNamePath(fieldName, name))
179
+ ) {
180
+ entries.delete(fieldName);
181
+ }
182
+ });
183
+ };
184
+
185
+ private clearFieldStatuses = (names?: NamePath[]) => {
186
+ this.deleteRelatedEntries(this.fieldStatuses, names);
187
+ };
188
+
117
189
  setFieldsValue = (newStore: Store, trigger: TRIGGER = 'onChange') => {
118
190
  if (trigger === 'reset' || trigger === 'clear') {
119
191
  this.store = newStore;
@@ -196,7 +268,10 @@ export class FormStore {
196
268
  });
197
269
  }
198
270
  this.callbacks?.onReset?.();
199
- this.validateMessages.clear();
271
+ // A named reset only clears rule messages of the fields it touched;
272
+ // unrelated fields keep their (still valid) validation state.
273
+ this.deleteRelatedEntries(this.validateMessages, name);
274
+ this.clearFieldStatuses(name);
200
275
  this.groupEntities.forEach((entity) => entity.reset());
201
276
  this.refreshField();
202
277
  };
@@ -208,6 +283,7 @@ export class FormStore {
208
283
  acc[name] = undefined;
209
284
  return acc;
210
285
  }, {} as Store);
286
+ this.clearFieldStatuses(name);
211
287
  this.setFieldsValue(clearStore, 'clear');
212
288
  };
213
289
 
@@ -567,7 +643,19 @@ export class FormStore {
567
643
  const { onFinish, onFinishFailed } = this.callbacks;
568
644
  const fieldErrors = await this.validateFields();
569
645
  const groupErrors = await this.validateGroups(true);
570
- const allErrors = [...fieldErrors, ...groupErrors];
646
+ const statusErrors = Array.from(this.fieldStatuses.entries())
647
+ .filter(
648
+ ([, status]) => status.status === 'error' && status.blocking === true,
649
+ )
650
+ .map(
651
+ ([name, status]): ValidateMessage => ({
652
+ name,
653
+ message: status.message ?? '',
654
+ value: this.getFieldValue(name),
655
+ status: 'error',
656
+ }),
657
+ );
658
+ const allErrors = [...fieldErrors, ...groupErrors, ...statusErrors];
571
659
  if (allErrors.length === 0) {
572
660
  onFinish?.(this.getFieldsValue());
573
661
  } else {
@@ -580,6 +668,8 @@ export class FormStore {
580
668
  getForm(): FormInstance {
581
669
  return {
582
670
  getFieldValidate: this.getFieldValidate,
671
+ getFieldStatus: this.getFieldStatus,
672
+ setFieldStatus: this.setFieldStatus,
583
673
  getFieldsValue: this.getFieldsValue,
584
674
  getFieldValue: this.getFieldValue,
585
675
  setFieldsValue: this.setFieldsValue,
@@ -21,6 +21,7 @@ High performance form component with data scope management.
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
23
  <code src="./demo/async-validate.tsx" title="Async Validation" description="async validator for server-side checks like uniqueness"></code>
24
+ <code src="./demo/field-status.tsx" title="Dynamic Field Status" description="Set error, warning, success, and info feedback dynamically with setFieldStatus"></code>
24
25
  <code src="./demo/list.tsx" title="Form List" description="Form list field usage"></code>
25
26
  <code src="./demo/form-table.tsx" title="Form Table" description="Form table field usage"></code>
26
27
  <code src="./demo/dynamic.tsx" title="Dynamic Form" description="Dynamic form items"></code>
@@ -52,12 +53,14 @@ High performance form component with data scope management.
52
53
  | Property | Type | Description |
53
54
  | --------------------- | ------------------------------------------------------------------------------------------ | ------------------------- |
54
55
  | getFieldValidate | `(name: Name) => ValidateMessage \| null` | Get field validation (may lag before async validation completes; updates via onStoreChange) |
56
+ | getFieldStatus | `(name: NamePath) => FieldStatusData \| null` | Get the displayed field status; rule validation errors take display precedence, otherwise a manually set status overrides validation |
57
+ | setFieldStatus | `(name: NamePath, status: FieldStatusData \| null) => void` | Set field status and message dynamically, or pass `null` to clear it; only blocking errors prevent submission |
55
58
  | getFieldValue | `(name: Name) => StoreValue` | Get field value |
56
59
  | submit | `() => Promise<ValidateMessage[]>` | Submit form (async, triggers onFinish only after validation completes) |
57
60
  | getFieldsValue | `() => Values` | Get all field values |
58
61
  | setFieldsValue | `(newStore: Store, trigger?: TRIGGER) => void` | Set field values |
59
62
  | setCallbacks | `(callbacks: Callbacks) => void` | Set callbacks |
60
- | resetFields | `(name?: Name[]) => void` | Reset fields |
63
+ | resetFields | `(name?: Name[]) => void` | Reset fields (values and validation state; passing name only affects related fields) |
61
64
  | clearFields | `(name?: Name[]) => void` | Clear fields |
62
65
  | validateField | `(name: Name, trigger?: 'onChange' \| 'onBlur') => Promise<ValidateMessage[]>` | Validate single field |
63
66
  | validateFields | `(names?: Name[], trigger?: TRIGGER) => Promise<ValidateMessage[]>` | Validate multiple fields |
@@ -129,6 +132,8 @@ High performance form component with data scope management.
129
132
  | Store | `Record<string, StoreValue>` |
130
133
  | Name | `string` |
131
134
  | NamePath | `string \| string[] \| number \| (string \| number)[]` |
135
+ | FieldStatus | `'error' \| 'success' \| 'warning' \| 'info'` |
136
+ | FieldStatusData | `{ status: FieldStatus; message?: string; blocking?: boolean }` |
132
137
  | ValidateMessage | `{ name: NamePath \| NamePath[] \| undefined; message: string; value: unknown; status: 'error' \| 'success' \| 'warning' \| 'info' }` |
133
138
  | FieldEntity | `{ props: fieldProps & { name?: NamePath }; onStoreChange: () => void }` |
134
139
  | TRIGGER | `'onChange' \| 'onBlur' \| 'onSubmit' \| 'reset' \| 'clear' \| 'update'` |
package/src/Form/index.ts CHANGED
@@ -29,6 +29,8 @@ Form.List = List;
29
29
 
30
30
  export type {
31
31
  FieldGroupProps,
32
+ FieldStatus,
33
+ FieldStatusData,
32
34
  FormInstance,
33
35
  FormListProps,
34
36
  FormProps,
@@ -23,6 +23,7 @@ toc: 'content'
23
23
  <code src="./demo/layout.tsx" title="表单布局" description="不同的表单布局"></code>
24
24
  <code src="./demo/validate.tsx" title="表单校验" description="表单校验功能"></code>
25
25
  <code src="./demo/async-validate.tsx" title="异步校验" description="async validator 实现服务端唯一性校验等异步场景"></code>
26
+ <code src="./demo/field-status.tsx" title="动态字段状态" description="通过 setFieldStatus 动态设置 error、warning、success 和 info 状态及提示"></code>
26
27
 
27
28
  ## API
28
29
 
@@ -51,12 +52,14 @@ toc: 'content'
51
52
  | 属性名 | 类型 | 描述 |
52
53
  | --------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------ |
53
54
  | getFieldValidate | `(name: Name) => ValidateMessage \| null` | 获取字段验证(异步校验未完成时可能读到旧值,最终会随 onStoreChange 更新) |
55
+ | getFieldStatus | `(name: NamePath) => FieldStatusData \| null` | 获取字段当前展示状态;规则校验错误优先展示,其余情况手动设置的状态优先于校验结果 |
56
+ | setFieldStatus | `(name: NamePath, status: FieldStatusData \| null) => void` | 动态设置字段状态和提示,传入 `null` 清除;仅 `blocking: true` 的 error 会阻止提交 |
54
57
  | getFieldValue | `(name: Name) => StoreValue` | 获取字段值 |
55
58
  | submit | `() => Promise<ValidateMessage[]>` | 提交表单(异步,校验完成后才触发 onFinish) |
56
59
  | getFieldsValue | `() => Values` | 获取所有字段值 |
57
60
  | setFieldsValue | `(newStore: Store, trigger?: TRIGGER) => void` | 设置字段值 |
58
61
  | setCallbacks | `(callbacks: Callbacks) => void` | 设置回调 |
59
- | resetFields | `(name?: Name[]) => void` | 重置字段 |
62
+ | resetFields | `(name?: Name[]) => void` | 重置字段(值与校验状态;指定 name 时只影响相关字段) |
60
63
  | clearFields | `(name?: Name[]) => void` | 清空字段 |
61
64
  | validateField | `(name: Name, trigger?: 'onChange' \| 'onBlur') => Promise<ValidateMessage[]>` | 校验单个字段 |
62
65
  | validateFields | `(names?: Name[], trigger?: TRIGGER) => Promise<ValidateMessage[]>` | 校验多个字段 |
@@ -128,6 +131,8 @@ toc: 'content'
128
131
  | Store | `Record<string, StoreValue>` |
129
132
  | Name | `string` |
130
133
  | NamePath | `string \| string[] \| number \| (string \| number)[]` |
134
+ | FieldStatus | `'error' \| 'success' \| 'warning' \| 'info'` |
135
+ | FieldStatusData | `{ status: FieldStatus; message?: string; blocking?: boolean }` |
131
136
  | ValidateMessage | `{ name: NamePath \| NamePath[] \| undefined; message: string; value: unknown; status: 'error' \| 'success' \| 'warning' \| 'info' }` |
132
137
  | FieldEntity | `{ props: fieldProps & { name?: NamePath }; onStoreChange: () => void }` |
133
138
  | TRIGGER | `'onChange' \| 'onBlur' \| 'onSubmit' \| 'reset' \| 'clear' \| 'update'` |
@@ -106,6 +106,15 @@ export interface FormInstance<Values = any> {
106
106
  * Get field validation info
107
107
  */
108
108
  getFieldValidate: (name: Name) => ValidateMessage | null;
109
+ /**
110
+ * Get the displayed field status. Rule validation errors take display
111
+ * precedence; otherwise a manually set status overrides validation.
112
+ */
113
+ getFieldStatus: (name: NamePath) => FieldStatusData | null;
114
+ /**
115
+ * Set or clear a field's displayed status. Pass null to clear it.
116
+ */
117
+ setFieldStatus: (name: NamePath, status: FieldStatusData | null) => void;
109
118
  /**
110
119
  * Get field value
111
120
  */
@@ -253,11 +262,20 @@ export type Rule = {
253
262
  | Array<'onBlur' | 'onChange' | 'onSubmit'>;
254
263
  };
255
264
 
265
+ export type FieldStatus = 'error' | 'success' | 'warning' | 'info';
266
+
267
+ export type FieldStatusData = {
268
+ status: FieldStatus;
269
+ message?: string;
270
+ /** Whether an error status should prevent form submission. */
271
+ blocking?: boolean;
272
+ };
273
+
256
274
  export type ValidateMessage = {
257
275
  name: NamePath | NamePath[] | undefined;
258
276
  message: string;
259
277
  value: unknown;
260
- status: 'error' | 'success' | 'warning' | 'info';
278
+ status: FieldStatus;
261
279
  };
262
280
 
263
281
  export type FieldConfigurableTooltip = Omit<TooltipProps, 'children'> & {
@@ -79,6 +79,18 @@
79
79
  &-error {
80
80
  color: var(--yee-error-color);
81
81
  }
82
+
83
+ &-warning {
84
+ color: var(--yee-warning-color);
85
+ }
86
+
87
+ &-success {
88
+ color: var(--yee-success-color);
89
+ }
90
+
91
+ &-info {
92
+ color: var(--yee-info-color);
93
+ }
82
94
  }
83
95
 
84
96
  // 表单校验态:在此重定义控件读取的 border / box-shadow 变量,
@@ -97,5 +109,19 @@
97
109
  --yee-input-focus-bs-color: var(--yee-warning-color-border);
98
110
  --yee-checkbox-border-color: var(--yee-warning-color);
99
111
  }
112
+
113
+ &&-success {
114
+ --yee-input-border-color: var(--yee-success-color);
115
+ --yee-input-hover-border-color: var(--yee-success-color-hover);
116
+ --yee-input-focus-bs-color: var(--yee-success-color-border);
117
+ --yee-checkbox-border-color: var(--yee-success-color);
118
+ }
119
+
120
+ &&-info {
121
+ --yee-input-border-color: var(--yee-info-color);
122
+ --yee-input-hover-border-color: var(--yee-info-color-hover);
123
+ --yee-input-focus-bs-color: var(--yee-info-color-border);
124
+ --yee-checkbox-border-color: var(--yee-info-color);
125
+ }
100
126
  }
101
127
  }
package/src/version.ts CHANGED
@@ -4,7 +4,7 @@ declare global {
4
4
  }
5
5
  }
6
6
 
7
- export const version = "0.8.20";
7
+ export const version = "0.9.0";
8
8
 
9
9
  if (typeof window !== 'undefined') {
10
10
  window.__yee_c_version__ = version;