@fairys/valtio-form-basic 0.0.8 → 0.0.9

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/src/form/form.tsx CHANGED
@@ -2,6 +2,7 @@ import { MObject } from 'interface';
2
2
  import { FairysValtioFormInstance, useFairysValtioFormInstance } from './instance';
3
3
  import { useMemo, type ReactNode } from 'react';
4
4
  import { FairysValtioFormLayoutAttrsProps } from './layout';
5
+ import { RuleItem } from 'async-validator';
5
6
 
6
7
  export interface FairysValtioFormAttrsProps<T extends MObject<T> = object> extends FairysValtioFormLayoutAttrsProps {
7
8
  /**表单实例*/
@@ -9,7 +10,7 @@ export interface FairysValtioFormAttrsProps<T extends MObject<T> = object> exten
9
10
  /**子元素*/
10
11
  children: ReactNode;
11
12
  /**表单项规则*/
12
- rules?: FairysValtioFormInstance<T>['rules'];
13
+ rules?: Record<PropertyKey, RuleItem[]>;
13
14
  /**表单初始值*/
14
15
  formData?: FairysValtioFormInstance<T>['state'];
15
16
  /**表单隐藏状态*/
@@ -49,20 +50,4 @@ export function useFairysValtioForm<T extends MObject<T> = object>(props: Fairys
49
50
  ...rest,
50
51
  formInstance,
51
52
  };
52
- // return (
53
- // <FairysValtioFormInstanceContext.Provider value={formInstance}>
54
- // <FairysValtioFormLayout {...rest}>{children}</FairysValtioFormLayout>
55
- // </FairysValtioFormInstanceContext.Provider>
56
- // );
57
53
  }
58
-
59
- // /**初始化实例*/
60
- // FairysValtioForm.useForm = useFairysValtioFormInstance;
61
- // /**获取状态*/
62
- // FairysValtioForm.useFormState = useFairysValtioFormInstanceContextState;
63
- // /**获取隐藏状态*/
64
- // FairysValtioForm.useFormHideState = useFairysValtioFormInstanceContextHideState;
65
- // /**获取上下文实例*/
66
- // FairysValtioForm.useFormInstance = useFairysValtioFormInstanceContext;
67
- // /**表单项*/
68
- // FairysValtioForm.useFormItemAttrs = useFairysValtioFormItemAttrs;
@@ -0,0 +1,78 @@
1
+ import { useRef, createContext, useContext, useMemo } from 'react';
2
+ import { proxy, useSnapshot } from 'valtio';
3
+ import { formatePath, formateName } from 'form/utils';
4
+ export interface FairysValtioFormParentAttrsState {
5
+ name?: string;
6
+ }
7
+
8
+ /***
9
+ * 父级属性
10
+ */
11
+ export class FairysValtioFormParentAttrs {
12
+ state = proxy<FairysValtioFormParentAttrsState>({
13
+ name: '',
14
+ });
15
+ // ===================================================更新状态================================================================
16
+ updated = (attrs: Record<string, any>) => {
17
+ this.state = { ...this.state, ...attrs };
18
+ };
19
+ /***更新父级字段值*/
20
+ updatedName = (name?: string, parentName?: string) => {
21
+ this.state.name = formateName(name, parentName);
22
+ };
23
+ }
24
+
25
+ /**初始化父级属性*/
26
+ export const useFairysValtioFormParentAttrs = (instance?: FairysValtioFormParentAttrs) => {
27
+ const parentAttrs = useRef<FairysValtioFormParentAttrs>();
28
+ if (!parentAttrs.current) {
29
+ if (instance) {
30
+ parentAttrs.current = instance;
31
+ } else {
32
+ parentAttrs.current = new FairysValtioFormParentAttrs();
33
+ }
34
+ }
35
+ return parentAttrs.current;
36
+ };
37
+
38
+ /***父级属性上下文*/
39
+ export const FairysValtioFormParentAttrsContext = createContext<FairysValtioFormParentAttrs>(
40
+ new FairysValtioFormParentAttrs(),
41
+ );
42
+
43
+ /***获取父级属性实例*/
44
+ export const useFairysValtioFormParentAttrsContext = () => useContext(FairysValtioFormParentAttrsContext);
45
+
46
+ /***获取父级属性状态*/
47
+ export const useFairysValtioFormParentAttrsState = () => {
48
+ const instance = useFairysValtioFormParentAttrsContext();
49
+ const state = useSnapshot(instance.state);
50
+ return [state, instance] as const;
51
+ };
52
+
53
+ export interface FairysValtioFormAttrsNameOptions {
54
+ name?: string;
55
+ /**是否拼接父级字段名*/
56
+ isJoinParentField?: boolean;
57
+ }
58
+
59
+ /***获取属性名和路径*/
60
+ export const useFairysValtioFormAttrsName = (options: FairysValtioFormAttrsNameOptions = {}) => {
61
+ const { name, isJoinParentField = true } = options;
62
+ const formAttrsNameInstance = useFairysValtioFormParentAttrs();
63
+ const [state] = useFairysValtioFormParentAttrsState();
64
+ const parentName = state.name;
65
+ const _name = useMemo(
66
+ () => (isJoinParentField ? formateName(name, parentName) : name),
67
+ [name, parentName, isJoinParentField],
68
+ );
69
+ const _paths = useMemo(() => formatePath(_name), [_name]);
70
+ useMemo(() => formAttrsNameInstance.updatedName(_name), [_name]);
71
+
72
+ return {
73
+ formAttrsNameInstance,
74
+ parentName,
75
+ name: _name,
76
+ paths: _paths,
77
+ };
78
+ };
@@ -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,53 @@ 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 列字段数组(可选)
120
- * @param isReturn 是否返回验证结果(可选)
121
- */
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
+ /**验证表单项规则*/
122
133
  validate = async (fields?: PropertyKey[], isReturn: boolean = true): Promise<ValidateFieldsError | Values> => {
123
- let _fields = fields;
134
+ const rules = {
135
+ ...this.rules,
136
+ ...this.mountRules,
137
+ };
138
+ // 根据字段路径获取对应的值
124
139
  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;
140
+ const _values: Record<PropertyKey, any> = {};
141
+ /**所有要验证的字段*/
142
+ let _fields = Object.keys(rules) as PropertyKey[];
143
+ /**最后要验证的规则*/
144
+ let _lastRules: Record<PropertyKey, RuleItem[]> = {};
145
+ /**是否指定了字段*/
146
+ let isPropsFields = false;
147
+ // 如果指定了字段,则只验证指定的字段
148
+ if (Array.isArray(fields) && fields.length) {
149
+ _fields = [...fields];
150
+ isPropsFields = true;
151
+ for (let index = 0; index < fields.length; index++) {
152
+ const field = fields[index];
153
+ const paths = this.nameToPaths[field];
154
+ _lastRules[field] = rules[field];
155
+ _values[field] = get(_formData, paths ? paths : formatePath(field as string));
156
+ }
157
+ } else {
158
+ isPropsFields = false;
159
+ _lastRules = { ...rules };
160
+ // 通过规则进行获取那些字段需要验证
161
+ for (let index = 0; index < _fields.length; index++) {
162
+ const field = _fields[index];
163
+ const paths = this.nameToPaths[field];
164
+ _values[field] = get(_formData, paths);
144
165
  }
145
- }
146
- if (!isNeedValidate) {
147
- console.warn('no rules to validate');
148
- return Promise.resolve({ ..._formData });
149
166
  }
150
167
  return new Promise((resolve, reject) => {
151
- new AsyncValidator({ ...rules }).validate({ ..._formData }, (errors, fields) => {
168
+ new AsyncValidator({ ...rules }).validate({ ..._values }, (errors, fields) => {
152
169
  for (let index = 0; index < _fields.length; index++) {
153
170
  const field = _fields[index];
154
171
  const fidError = Array.isArray(errors) ? errors.filter((item) => item.field === field) : undefined;
@@ -162,7 +179,12 @@ export class FairysValtioFormInstance<T extends MObject<T> = Record<string, any>
162
179
  if (errors) {
163
180
  reject({ errors, fields });
164
181
  } else {
165
- resolve(fields);
182
+ /**如果是指定字段,直接返回字段值*/
183
+ if (isPropsFields) {
184
+ resolve({ ...fields });
185
+ } else {
186
+ resolve({ ..._formData });
187
+ }
166
188
  }
167
189
  }
168
190
  });
@@ -27,7 +27,13 @@ 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
+ showColon?: boolean;
31
37
  }
32
38
 
33
39
  export interface FairysValtioFormLayoutAttrsProps extends FairysValtioFormLayoutContextOptions {
@@ -66,7 +72,7 @@ export class FairysValtioFormLayoutInstance {
66
72
  colCount: 1,
67
73
  errorLayout: 'right-bottom',
68
74
  labelMode: 'between',
69
- borderedType: 'bottom',
75
+ itemBorderType: 'bottom',
70
76
  });
71
77
  updated = (options: FairysValtioFormLayoutContextOptions = {}) => {
72
78
  const keys = Object.keys(options);
@@ -156,7 +162,10 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
156
162
  const parent_formItemLabelStyle = state.formItemLabelStyle;
157
163
  const parent_formItemBodyClassName = state.formItemBodyClassName;
158
164
  const parent_formItemBodyStyle = state.formItemBodyStyle;
159
- const parent_borderedType = state.borderedType || 'bottom';
165
+ const parent_borderedType = state.itemBorderType || 'bottom';
166
+ const parent_itemBorderColor = state.itemBorderColor;
167
+ const parent_isInvalidBorderRed = state.isInvalidBorderRed;
168
+ const parent_showColon = state.showColon;
160
169
 
161
170
  const {
162
171
  colCount = parent_colCount,
@@ -168,8 +177,11 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
168
177
  formItemLabelStyle = parent_formItemLabelStyle,
169
178
  formItemBodyClassName = parent_formItemBodyClassName,
170
179
  formItemBodyStyle = parent_formItemBodyStyle,
171
- borderedType = parent_borderedType,
180
+ itemBorderType = parent_borderedType,
181
+ itemBorderColor = parent_itemBorderColor,
172
182
  lastItemBordered = true,
183
+ isInvalidBorderRed = parent_isInvalidBorderRed,
184
+ showColon = parent_showColon,
173
185
  gap,
174
186
  isAllColSpan = false,
175
187
  className,
@@ -194,7 +206,10 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
194
206
  formItemLabelStyle,
195
207
  formItemBodyClassName,
196
208
  formItemBodyStyle,
197
- borderedType,
209
+ itemBorderType,
210
+ itemBorderColor,
211
+ isInvalidBorderRed,
212
+ showColon,
198
213
  }),
199
214
  [
200
215
  colCount,
@@ -206,17 +221,20 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
206
221
  formItemLabelStyle,
207
222
  formItemBodyClassName,
208
223
  formItemBodyStyle,
209
- borderedType,
224
+ itemBorderType,
225
+ itemBorderColor,
226
+ isInvalidBorderRed,
227
+ showColon,
210
228
  ],
211
229
  );
212
230
 
213
231
  const layoutCls = useMemo(
214
232
  () =>
215
233
  clsx(
216
- `fairys-valtio-form-layout fairystaroform__text-[12px] fairystaroform__w-full fairystaroform__box-border fairystaroform__rounded-md`,
234
+ `fairys-valtio-form-layout fairystaroform__text-[12px] fairystaroform__w-full fairystaroform__box-border fairystaroform__rounded-[4px]`,
217
235
  {
218
236
  'fairys-valtio-form-layout-all-col-span': isAllColSpan,
219
- 'fairys-taro-form-valtio-layout-box-shadow': boxShadow,
237
+ 'fairys-valtio-form-layout-box-shadow': boxShadow,
220
238
  'fairystaroform__border fairystaroform__border-solid fairystaroform__border-gray-200': bordered,
221
239
  'fairys-valtio-form-layout-last-item-no-border': !lastItemBordered,
222
240
  },
@@ -270,7 +288,7 @@ export function useFairysValtioFormLayoutAttrs(props: FairysValtioFormLayoutAttr
270
288
  colCount,
271
289
  errorLayout,
272
290
  labelMode,
273
- borderedType,
291
+ itemBorderType,
274
292
  formLayoutInstance,
275
293
  //======================
276
294
  layoutName: layoutCls,
@@ -299,7 +317,7 @@ export interface FairysValtioFormLayoutAttrsReturn {
299
317
  /**
300
318
  * 底部边框类型
301
319
  */
302
- borderedType: string;
320
+ itemBorderType: string;
303
321
  /**表单布局实例*/
304
322
  formLayoutInstance: FairysValtioFormLayoutInstance;
305
323
  /**布局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,17 @@
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
+ }