@fairys/valtio-form-basic 0.0.8 → 0.0.10

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.
@@ -3,6 +3,7 @@ import { createContext, useContext, useRef } from 'react';
3
3
  import { proxy, ref, snapshot, useSnapshot } from 'valtio';
4
4
  import AsyncValidator, { RuleItem, ValidateFieldsError, Values } from 'async-validator';
5
5
  import { copy } from 'fast-copy';
6
+ import { formatePath, get, set } from 'form/utils';
6
7
 
7
8
  /**表单实例*/
8
9
  export class FairysValtioFormInstance<T extends MObject<T> = Record<string, any>> {
@@ -42,6 +43,14 @@ export class FairysValtioFormInstance<T extends MObject<T> = Record<string, any>
42
43
  }
43
44
  };
44
45
 
46
+ /**根据路径设置值
47
+ * @param path 值路径
48
+ * @param value 值
49
+ */
50
+ updatedValueByPaths = (path: PropertyKey, value: any) => {
51
+ set(this.state, formatePath(path), value);
52
+ this.validate([path], false);
53
+ };
45
54
  // ===================================================隐藏状态================================================================
46
55
  /**
47
56
  * 更新行数据的隐藏信息
@@ -110,45 +119,56 @@ export class FairysValtioFormInstance<T extends MObject<T> = Record<string, any>
110
119
  };
111
120
 
112
121
  // ===================================================规则处理================================================================
113
- /**列规则 */
114
- rules: Record<
115
- PropertyKey,
116
- ((formData: T, instance: FairysValtioFormInstance<T>) => RuleItem[] | Promise<RuleItem[]>) | RuleItem[]
117
- > = {};
118
- /**规则验证
119
- * @param fields 列字段数组(可选)
122
+ /**由表单项挂载规则,(根据表单项的字段存储路径对应校验规则)*/
123
+ mountRules: Record<PropertyKey, RuleItem[]> = {};
124
+ /**移除表单项挂载规则*/
125
+ removeRules = (name: PropertyKey) => {
126
+ delete this.mountRules[name];
127
+ };
128
+ /**表单项规则*/
129
+ rules: Record<PropertyKey, RuleItem[]> = {};
130
+ /**表单项名称到路径映射*/
131
+ nameToPaths: Record<PropertyKey, PropertyKey[]> = {};
132
+ /**验证表单项规则
133
+ * @param fields 要验证的字段(可选)
120
134
  * @param isReturn 是否返回验证结果(可选)
121
135
  */
122
136
  validate = async (fields?: PropertyKey[], isReturn: boolean = true): Promise<ValidateFieldsError | Values> => {
123
- let _fields = fields;
137
+ const rules = {
138
+ ...this.rules,
139
+ ...this.mountRules,
140
+ };
141
+ // 根据字段路径获取对应的值
124
142
  const _formData = snapshot(this.state) as T;
125
- // 没有规则,直接返回数据
126
- if (!this.rules) {
127
- return Promise.resolve({ ..._formData });
128
- }
129
- const rules: Record<PropertyKey, RuleItem[]> = {};
130
- let isNeedValidate = false;
131
- // 没有 fields 值,验证所有
132
- if (!fields || (Array.isArray(fields) && fields.length === 0)) {
133
- _fields = Object.keys(this.rules);
134
- }
135
- for (let index = 0; index < _fields.length; index++) {
136
- isNeedValidate = true;
137
- const element = _fields[index];
138
- const rule = this.rules[element];
139
- if (typeof rule === 'function') {
140
- const _rules = await rule(_formData, this);
141
- rules[element] = _rules;
142
- } else if (Array.isArray(rule)) {
143
- rules[element] = rule;
143
+ const _values: Record<PropertyKey, any> = {};
144
+ /**所有要验证的字段*/
145
+ let _fields = Object.keys(rules) as PropertyKey[];
146
+ /**最后要验证的规则*/
147
+ let _lastRules: Record<PropertyKey, RuleItem[]> = {};
148
+ /**是否指定了字段*/
149
+ let isPropsFields = false;
150
+ // 如果指定了字段,则只验证指定的字段
151
+ if (Array.isArray(fields) && fields.length) {
152
+ _fields = [...fields];
153
+ isPropsFields = true;
154
+ for (let index = 0; index < fields.length; index++) {
155
+ const field = fields[index];
156
+ const paths = this.nameToPaths[field];
157
+ _lastRules[field] = rules[field];
158
+ _values[field] = get(_formData, paths ? paths : formatePath(field as string));
159
+ }
160
+ } else {
161
+ isPropsFields = false;
162
+ _lastRules = { ...rules };
163
+ // 通过规则进行获取那些字段需要验证
164
+ for (let index = 0; index < _fields.length; index++) {
165
+ const field = _fields[index];
166
+ const paths = this.nameToPaths[field];
167
+ _values[field] = get(_formData, paths);
144
168
  }
145
- }
146
- if (!isNeedValidate) {
147
- console.warn('no rules to validate');
148
- return Promise.resolve({ ..._formData });
149
169
  }
150
170
  return new Promise((resolve, reject) => {
151
- new AsyncValidator({ ...rules }).validate({ ..._formData }, (errors, fields) => {
171
+ new AsyncValidator({ ...rules }).validate({ ..._values }, (errors, fields) => {
152
172
  for (let index = 0; index < _fields.length; index++) {
153
173
  const field = _fields[index];
154
174
  const fidError = Array.isArray(errors) ? errors.filter((item) => item.field === field) : undefined;
@@ -162,12 +182,28 @@ export class FairysValtioFormInstance<T extends MObject<T> = Record<string, any>
162
182
  if (errors) {
163
183
  reject({ errors, fields });
164
184
  } else {
165
- resolve(fields);
185
+ /**如果是指定字段,直接返回字段值*/
186
+ if (isPropsFields) {
187
+ resolve({ ...fields });
188
+ } else {
189
+ resolve({ ..._formData });
190
+ }
166
191
  }
167
192
  }
168
193
  });
169
194
  });
170
195
  };
196
+
197
+ /**
198
+ * 验证某些前缀的字段
199
+ * @param prefix 前缀字段数组
200
+ * @param isReturn 是否返回验证结果(可选)
201
+ */
202
+ validatePrefixFields = async (prefix: string[], isReturn: boolean = true): Promise<ValidateFieldsError | Values> => {
203
+ const fields = Object.keys(this.rules) as PropertyKey[];
204
+ const _fields = fields.filter((item) => prefix.some((p) => item.toString().startsWith(p)));
205
+ return this.validate(_fields, isReturn);
206
+ };
171
207
  }
172
208
 
173
209
  /**声明实例*/
@@ -6,7 +6,7 @@ export interface FairysValtioFormLayoutContextOptions {
6
6
  /**列数据*/
7
7
  colCount?: number;
8
8
  /**规则校验失败错误提示位置*/
9
- errorLayout?: 'left-bottom' | 'right-bottom' | 'top-right' | 'top-left';
9
+ errorLayout?: 'bottom-left' | 'bottom-right' | 'top-right' | 'top-left' | 'left-border-top' | 'right-border-top';
10
10
  /**
11
11
  * label显示模式
12
12
  * @platform taro 支持 between
@@ -27,7 +27,15 @@ export interface FairysValtioFormLayoutContextOptions {
27
27
  /**
28
28
  * 底部边框类型
29
29
  */
30
- borderedType?: 'bottom' | 'body';
30
+ itemBorderType?: 'bottom' | 'body';
31
+ /**边框颜色*/
32
+ itemBorderColor?: React.CSSProperties['borderColor'];
33
+ /**是否校验失败时显示红色边框*/
34
+ isInvalidBorderRed?: boolean;
35
+ /**是否校验失败时显示红色文本*/
36
+ isInvalidTextRed?: boolean;
37
+ /**是否显示冒号*/
38
+ showColon?: boolean;
31
39
  }
32
40
 
33
41
  export interface FairysValtioFormLayoutAttrsProps extends FairysValtioFormLayoutContextOptions {
@@ -64,9 +72,9 @@ export interface FairysValtioFormLayoutAttrsProps extends FairysValtioFormLayout
64
72
  export class FairysValtioFormLayoutInstance {
65
73
  state = proxy<FairysValtioFormLayoutContextOptions>({
66
74
  colCount: 1,
67
- errorLayout: 'right-bottom',
75
+ errorLayout: 'bottom-right',
68
76
  labelMode: 'between',
69
- borderedType: 'bottom',
77
+ itemBorderType: 'bottom',
70
78
  });
71
79
  updated = (options: FairysValtioFormLayoutContextOptions = {}) => {
72
80
  const keys = Object.keys(options);
@@ -148,7 +156,7 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
148
156
  const formLayoutInstance = useFairysValtioFormLayoutInstance();
149
157
  const [state] = useFairysValtioFormLayoutContext();
150
158
  const parent_colCount = state.colCount || 1;
151
- const parent_errorLayout = state.errorLayout || 'right-bottom';
159
+ const parent_errorLayout = state.errorLayout || 'bottom-right';
152
160
  const parent_labelMode = state.labelMode || 'between';
153
161
  const parent_formItemClassName = state.formItemClassName;
154
162
  const parent_formItemStyle = state.formItemStyle;
@@ -156,7 +164,11 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
156
164
  const parent_formItemLabelStyle = state.formItemLabelStyle;
157
165
  const parent_formItemBodyClassName = state.formItemBodyClassName;
158
166
  const parent_formItemBodyStyle = state.formItemBodyStyle;
159
- const parent_borderedType = state.borderedType || 'bottom';
167
+ const parent_borderedType = state.itemBorderType || 'bottom';
168
+ const parent_itemBorderColor = state.itemBorderColor;
169
+ const parent_isInvalidBorderRed = state.isInvalidBorderRed;
170
+ const parent_isInvalidTextRed = state.isInvalidTextRed;
171
+ const parent_showColon = state.showColon;
160
172
 
161
173
  const {
162
174
  colCount = parent_colCount,
@@ -168,8 +180,12 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
168
180
  formItemLabelStyle = parent_formItemLabelStyle,
169
181
  formItemBodyClassName = parent_formItemBodyClassName,
170
182
  formItemBodyStyle = parent_formItemBodyStyle,
171
- borderedType = parent_borderedType,
183
+ itemBorderType = parent_borderedType,
184
+ itemBorderColor = parent_itemBorderColor,
172
185
  lastItemBordered = true,
186
+ isInvalidBorderRed = parent_isInvalidBorderRed,
187
+ isInvalidTextRed = parent_isInvalidTextRed,
188
+ showColon = parent_showColon,
173
189
  gap,
174
190
  isAllColSpan = false,
175
191
  className,
@@ -194,7 +210,11 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
194
210
  formItemLabelStyle,
195
211
  formItemBodyClassName,
196
212
  formItemBodyStyle,
197
- borderedType,
213
+ itemBorderType,
214
+ itemBorderColor,
215
+ isInvalidBorderRed,
216
+ isInvalidTextRed,
217
+ showColon,
198
218
  }),
199
219
  [
200
220
  colCount,
@@ -206,17 +226,21 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
206
226
  formItemLabelStyle,
207
227
  formItemBodyClassName,
208
228
  formItemBodyStyle,
209
- borderedType,
229
+ itemBorderType,
230
+ itemBorderColor,
231
+ isInvalidBorderRed,
232
+ isInvalidTextRed,
233
+ showColon,
210
234
  ],
211
235
  );
212
236
 
213
237
  const layoutCls = useMemo(
214
238
  () =>
215
239
  clsx(
216
- `fairys-valtio-form-layout fairystaroform__text-[12px] fairystaroform__w-full fairystaroform__box-border fairystaroform__rounded-md`,
240
+ `fairys-valtio-form-layout fairystaroform__transition-all fairystaroform__duration-300 fairystaroform__text-[12px] fairystaroform__w-full fairystaroform__box-border fairystaroform__rounded-[8px]`,
217
241
  {
218
242
  'fairys-valtio-form-layout-all-col-span': isAllColSpan,
219
- 'fairys-taro-form-valtio-layout-box-shadow': boxShadow,
243
+ 'fairys-valtio-form-layout-box-shadow': boxShadow,
220
244
  'fairystaroform__border fairystaroform__border-solid fairystaroform__border-gray-200': bordered,
221
245
  'fairys-valtio-form-layout-last-item-no-border': !lastItemBordered,
222
246
  },
@@ -227,7 +251,7 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
227
251
  const headerCls = useMemo(
228
252
  () =>
229
253
  clsx(
230
- `fairys-valtio-form-layout-header fairystaroform__flex fairystaroform__justify-between fairystaroform__items-center fairystaroform__flex-row fairystaroform__py-[12px] fairystaroform__border-b fairystaroform__border-b-solid fairystaroform__border-b-gray-200 fairystaroform__box-border`,
254
+ `fairys-valtio-form-layout-header fairystaroform__transition-all fairystaroform__duration-300 fairystaroform__flex fairystaroform__justify-between fairystaroform__items-center fairystaroform__flex-row fairystaroform__py-[10px] fairystaroform__border-b fairystaroform__border-b-solid fairystaroform__border-b-gray-200 fairystaroform__box-border`,
231
255
  {
232
256
  'fairystaroform__px-[8px]': bordered || boxShadow,
233
257
  'fairystaroform__px-[4px]': !bordered && !boxShadow,
@@ -239,15 +263,21 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
239
263
  const headerTitleCls = useMemo(
240
264
  () =>
241
265
  clsx(
242
- `fairys-valtio-form-layout-header-title fairystaroform__text-[14px] fairystaroform__font-bold fairystaroform__box-border`,
266
+ `fairys-valtio-form-layout-header-title fairystaroform__transition-all fairystaroform__duration-300 fairystaroform__text-[14px] fairystaroform__font-bold fairystaroform__box-border`,
267
+ ),
268
+ [],
269
+ );
270
+ const headerExtraCls = useMemo(
271
+ () =>
272
+ clsx(
273
+ `fairys-valtio-form-layout-header-extra fairystaroform__transition-all fairystaroform__duration-300 fairystaroform__box-border`,
243
274
  ),
244
275
  [],
245
276
  );
246
- const headerExtraCls = useMemo(() => clsx(`fairys-valtio-form-layout-header-extra fairystaroform__box-border`), []);
247
277
 
248
278
  const body_base = useMemo(() => {
249
279
  return clsx(
250
- 'fairys-valtio-form-layout-body fairystaroform__px-[8px] fairystaroform__w-full fairystaroform__grid fairystaroform__gap-[2px] fairystaroform__box-border',
280
+ 'fairys-valtio-form-layout-body fairystaroform__transition-all fairystaroform__duration-300 fairystaroform__px-[8px] fairystaroform__w-full fairystaroform__grid fairystaroform__gap-[2px] fairystaroform__box-border',
251
281
  bodyClassName,
252
282
  );
253
283
  }, [bodyClassName]);
@@ -270,7 +300,7 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
270
300
  colCount,
271
301
  errorLayout,
272
302
  labelMode,
273
- borderedType,
303
+ itemBorderType,
274
304
  formLayoutInstance,
275
305
  //======================
276
306
  layoutName: layoutCls,
@@ -299,7 +329,7 @@ export interface FairysValtioFormLayoutAttrsReturn {
299
329
  /**
300
330
  * 底部边框类型
301
331
  */
302
- borderedType: string;
332
+ itemBorderType: string;
303
333
  /**表单布局实例*/
304
334
  formLayoutInstance: FairysValtioFormLayoutInstance;
305
335
  /**布局ClassName*/
@@ -0,0 +1,108 @@
1
+ /***
2
+ * 设置值
3
+ * @param object 任意对象
4
+ * @param paths 值路径
5
+ * @param nextValue 新值
6
+ *
7
+ * @description
8
+ * 值不存在时,当 paths 路径中的值为 number 类型时,会创建一个空数组。当 paths 路径中的值为 string 类型时,会创建一个空对象。
9
+ */
10
+ export function set<T>(state: any, paths: PropertyKey[], nextValue: T) {
11
+ const _keys = [...paths];
12
+ let current: any = state;
13
+ const length = _keys.length - 1;
14
+ for (let i = 0; i <= length; i++) {
15
+ const key = _keys[i];
16
+ const _current = current[key];
17
+ // 应该判断下一个key的类型,而不是当前key的类型
18
+ const nextKey = _keys[i + 1];
19
+ /**
20
+ * 如果下一个key不存在,且key是数字,那么创建一个空数组
21
+ */
22
+ if (typeof _current === 'undefined' && typeof nextKey === 'number') {
23
+ current[key] = [];
24
+ } else if (typeof _current === 'undefined' && typeof nextKey === 'string') {
25
+ /**
26
+ * 如果下一个key不存在,且key是字符串,那么创建一个空对象
27
+ */
28
+ current[key] = {};
29
+ }
30
+ // 判断最后一个,直接赋值
31
+ if (i === length) {
32
+ current[key] = nextValue;
33
+ } else {
34
+ current = current[key];
35
+ }
36
+ }
37
+ return state;
38
+ }
39
+
40
+ /***
41
+ * 获取值
42
+ * @param value 任意值
43
+ * @param segments 键路径
44
+ */
45
+ export function get<TDefault = unknown>(value: any, segments: PropertyKey[]): TDefault {
46
+ let current: any = value;
47
+ for (const key of segments) {
48
+ current = current?.[key];
49
+ }
50
+ return current;
51
+ }
52
+
53
+ /***
54
+ * 格式化路径,将路径中的数组索引转换为数字
55
+ * @param path 路径
56
+ * @returns 格式化后的路径
57
+ */
58
+ export function formatePath(path: PropertyKey) {
59
+ if (typeof path !== 'string') {
60
+ return [path];
61
+ }
62
+ return path
63
+ .split(/[\.]/g)
64
+ .reduce((pre, next) => {
65
+ if (/\[[0-9]+\]$/.test(next)) {
66
+ const _next = next.split(/\[/);
67
+ let _nextValue: (string | number)[] = [];
68
+ for (let index = 0; index < _next.length; index++) {
69
+ const element = _next[index];
70
+ if (/\]$/.test(element)) {
71
+ // 去掉数组索引的方括号
72
+ const _v = element.replace(/\]$/, '');
73
+ // 转换为数字
74
+ const v = Number.parseInt(_v);
75
+ /**判断转换后和转换前的长度是否一致,一致则说明是数字*/
76
+ if (_v.length === `${v}`.length && !Number.isNaN(v)) {
77
+ _nextValue.push(v);
78
+ } else {
79
+ _nextValue.push(_v);
80
+ }
81
+ } else {
82
+ _nextValue.push(element);
83
+ }
84
+ }
85
+ return [...pre, ..._nextValue];
86
+ }
87
+ return [...pre, next];
88
+ }, [] as (string | number)[])
89
+ .map((item) => {
90
+ if (typeof item === 'string') {
91
+ return item.trim();
92
+ }
93
+ return item;
94
+ })
95
+ .filter((item) => item !== '');
96
+ }
97
+
98
+ /**格式化属性名*/
99
+ export function formateName(name?: string, parentName?: string) {
100
+ if (parentName && name) {
101
+ return parentName + '.' + name;
102
+ } else if (parentName) {
103
+ return parentName;
104
+ } else if (name) {
105
+ return name;
106
+ }
107
+ return '';
108
+ }
package/src/index.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './form/hooks';
1
2
  export * from './form/instance';
2
3
  export * from './form/layout';
3
4
  export * from './form/form';
@@ -19,11 +19,11 @@
19
19
  box-sizing: border-box;
20
20
  }
21
21
 
22
- .fairys-valtio-form-layout.fairys-form-layout-all-col-span {
22
+ .fairys-valtio-form-layout.fairys-valtio-form-layout-all-col-span {
23
23
  grid-column: 1 / -1;
24
24
  }
25
25
 
26
- .fairys-valtio-form-layout.fairys-form-layout-box-shadow {
26
+ .fairys-valtio-form-layout.fairys-valtio-form-layout-box-shadow {
27
27
  box-shadow: 0 6px 16px -8px #00000014, 0 9px 28px #0000000d, 0 12px 48px 16px #00000008;
28
28
  }
29
29
 
@@ -31,11 +31,25 @@
31
31
  margin-top: 12px;
32
32
  }
33
33
 
34
- .fairys-valtio-form-layout-last-item-no-border > .fairys-valtio-form-layout-body > .fairys-valtio-form-item:last-child,
34
+ .fairys-valtio-form-layout-last-item-no-border
35
+ > .fairys-valtio-form-layout-body
36
+ > .fairys-valtio-form-item:last-child:not(.fairys-valtio-form-item-invalid-border-red),
35
37
  .fairys-valtio-form-layout-last-item-no-border
36
38
  > .fairys-valtio-form-layout-body
37
39
  > .fairys-valtio-form-item:last-child
38
40
  > .fairys-valtio-form-item-container
39
- > .fairys-valtio-form-item-body {
41
+ > .fairys-valtio-form-item-body:not(.fairys-valtio-form-item-invalid-border-red) {
40
42
  border-bottom: 0px;
41
43
  }
44
+
45
+ .fairys-valtio-form-item-invalid-border-red {
46
+ border-bottom-color: red !important;
47
+ }
48
+
49
+ .fairys-valtio-form-item-invalid-text-red > .fairys-valtio-form-item-container > .fairys-valtio-form-item-label,
50
+ .fairys-valtio-form-item-invalid-text-red
51
+ > .fairys-valtio-form-item-container
52
+ > .fairys-valtio-form-item-body
53
+ > .fairys-valtio-form-item-body-input {
54
+ color: red !important;
55
+ }