@flowgram-vue/form 0.2.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/LICENSE +22 -0
- package/dist/index.cjs +0 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +0 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
- package/src/composables/index.ts +13 -0
- package/src/composables/use-current-field-state.ts +29 -0
- package/src/composables/use-current-field.ts +31 -0
- package/src/composables/use-field-validate.ts +25 -0
- package/src/composables/use-field.ts +47 -0
- package/src/composables/use-form-model.ts +17 -0
- package/src/composables/use-form-state.ts +25 -0
- package/src/composables/use-form.ts +16 -0
- package/src/composables/use-watch.ts +34 -0
- package/src/constants.ts +34 -0
- package/src/core/create-form.ts +58 -0
- package/src/core/field-array-model.ts +333 -0
- package/src/core/field-model.ts +396 -0
- package/src/core/form-model.ts +362 -0
- package/src/core/index.ts +14 -0
- package/src/core/path.ts +125 -0
- package/src/core/store.ts +33 -0
- package/src/core/to-field-array.ts +52 -0
- package/src/core/to-field.ts +80 -0
- package/src/core/to-form.ts +64 -0
- package/src/core/utils.ts +123 -0
- package/src/env.d.ts +10 -0
- package/src/index.ts +30 -0
- package/src/types/common.ts +6 -0
- package/src/types/field.ts +193 -0
- package/src/types/form.ts +145 -0
- package/src/types/index.ts +8 -0
- package/src/types/validate.ts +88 -0
- package/src/utils/dom.ts +31 -0
- package/src/utils/event.ts +32 -0
- package/src/utils/glob.ts +279 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/object.ts +107 -0
- package/src/utils/validate.ts +42 -0
- package/src/vue/context.ts +12 -0
- package/src/vue/field-array.vue +117 -0
- package/src/vue/field.vue +127 -0
- package/src/vue/form.vue +61 -0
- package/src/vue/index.ts +11 -0
- package/src/vue/types.ts +45 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Emitter } from '@flowgram-vue/utils';
|
|
7
|
+
|
|
8
|
+
interface Payload<T> {
|
|
9
|
+
origin?: T;
|
|
10
|
+
current?: T;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class EmitterChain<T> {
|
|
14
|
+
protected emitter: Emitter<Payload<T>>;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
this.emitter = new Emitter<Payload<T>>();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get event() {
|
|
21
|
+
return this.emitter.event;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_fire(current?: T, origin?: T) {
|
|
25
|
+
this.emitter.fire({ current, origin });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fire(current: T, next?: EmitterChain<T>) {
|
|
29
|
+
this._fire(current);
|
|
30
|
+
next?._fire(undefined, current);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { flatten, get, isArray, isObject } from 'lodash-es';
|
|
7
|
+
|
|
8
|
+
export namespace Glob {
|
|
9
|
+
export const DIVIDER = '.';
|
|
10
|
+
export const ALL = '*';
|
|
11
|
+
|
|
12
|
+
// 仅支持一个通配符
|
|
13
|
+
export function isMatch(pattern: string, path: string) {
|
|
14
|
+
const patternArr = pattern.split(DIVIDER);
|
|
15
|
+
const pathArr = path.split(DIVIDER);
|
|
16
|
+
if (patternArr.length !== pathArr.length) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return patternArr.every((pattern, index) => {
|
|
20
|
+
if (pattern === ALL) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return pattern === pathArr[index];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 判断pattern 是否match pattern 或其parent
|
|
29
|
+
* @param pattern
|
|
30
|
+
* @param path
|
|
31
|
+
*/
|
|
32
|
+
export function isMatchOrParent(pattern: string, path: string) {
|
|
33
|
+
if (pattern === '') {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const patternArr = pattern.split(DIVIDER);
|
|
37
|
+
const pathArr = path.split(DIVIDER);
|
|
38
|
+
|
|
39
|
+
if (patternArr.length > pathArr.length) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < patternArr.length; i++) {
|
|
44
|
+
if (patternArr[i] !== ALL && patternArr[i] !== pathArr[i]) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 从 path 中提取出匹配pattern 的 parent path,包括是 path 自身
|
|
53
|
+
* 该方法默认 isMatchOrParent(pattern, path) 为 true, 不做为false 的错误处理。
|
|
54
|
+
* @param pattern
|
|
55
|
+
* @param path
|
|
56
|
+
*/
|
|
57
|
+
export function getParentPathByPattern(pattern: string, path: string) {
|
|
58
|
+
const patternArr = pattern.split(DIVIDER);
|
|
59
|
+
const pathArr = path.split(DIVIDER);
|
|
60
|
+
|
|
61
|
+
return pathArr.slice(0, patternArr.length).join(DIVIDER);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function concatPath(p1: string | number, ...pathArr: (string | number)[]): string {
|
|
65
|
+
const p2 = pathArr.shift();
|
|
66
|
+
if (p2 === undefined) return p1.toString();
|
|
67
|
+
let resultPath = '';
|
|
68
|
+
if (p1 === '' && p2 === '') {
|
|
69
|
+
resultPath = '';
|
|
70
|
+
} else if (p1 !== '' && p2 === '') {
|
|
71
|
+
resultPath = p1.toString();
|
|
72
|
+
} else if (p1 === '' && p2 !== '') {
|
|
73
|
+
resultPath = p2.toString();
|
|
74
|
+
} else {
|
|
75
|
+
resultPath = `${p1}${DIVIDER}${p2}`;
|
|
76
|
+
}
|
|
77
|
+
if (pathArr.length > 0) {
|
|
78
|
+
return concatPath(resultPath, ...pathArr);
|
|
79
|
+
}
|
|
80
|
+
return resultPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 找到 obj 在给与 paths 下所有子path
|
|
85
|
+
* @param paths
|
|
86
|
+
* @param obj
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
export function getSubPaths(paths: string[], obj: any): string[] {
|
|
90
|
+
if (!obj || typeof obj !== 'object') {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return flatten(
|
|
95
|
+
paths.map((path) => {
|
|
96
|
+
const value = path === '' ? obj : get(obj, path);
|
|
97
|
+
if (isArray(value)) {
|
|
98
|
+
return value.map((_: any, index: number) => concatPath(path, index));
|
|
99
|
+
} else if (isObject(value)) {
|
|
100
|
+
return Object.keys(value).map((key) => concatPath(path, key));
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 将带有通配符的 path pattern 分割。如 a.b.*.c.*.d, 会被分割成['a.b','*','c','*','d']
|
|
109
|
+
* @param pattern
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
export function splitPattern(pattern: string): string[] {
|
|
113
|
+
const parts = pattern.split(DIVIDER);
|
|
114
|
+
const res: string[] = [];
|
|
115
|
+
|
|
116
|
+
let i = 0;
|
|
117
|
+
let curPath: string[] = [];
|
|
118
|
+
|
|
119
|
+
while (i < parts.length) {
|
|
120
|
+
if (parts[i] === ALL) {
|
|
121
|
+
if (curPath.length) {
|
|
122
|
+
res.push(curPath.join(DIVIDER));
|
|
123
|
+
}
|
|
124
|
+
res.push(ALL);
|
|
125
|
+
curPath = [];
|
|
126
|
+
} else {
|
|
127
|
+
curPath.push(parts[i]);
|
|
128
|
+
}
|
|
129
|
+
i += 1;
|
|
130
|
+
}
|
|
131
|
+
if (curPath.length) {
|
|
132
|
+
res.push(curPath.join(DIVIDER));
|
|
133
|
+
}
|
|
134
|
+
return res;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Find all paths matched pattern in object. If withEmptyValue is true, it will include
|
|
139
|
+
* paths whoes value is undefined.
|
|
140
|
+
* @param obj
|
|
141
|
+
* @param pattern
|
|
142
|
+
* @param withEmptyValue
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
export function findMatchPaths(obj: any, pattern: string, withEmptyValue?: boolean): string[] {
|
|
146
|
+
if (!obj || !pattern) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const nextPaths: string[] = pattern.split(DIVIDER);
|
|
150
|
+
let curKey: string | undefined = nextPaths.shift();
|
|
151
|
+
let curPaths: string[] = [];
|
|
152
|
+
let curValue = obj;
|
|
153
|
+
while (curKey) {
|
|
154
|
+
let isObject = typeof curValue === 'object' && curValue !== null;
|
|
155
|
+
if (!isObject) return [];
|
|
156
|
+
// 匹配 *
|
|
157
|
+
if (curKey === ALL) {
|
|
158
|
+
const parentPath = curPaths.join(DIVIDER);
|
|
159
|
+
return flatten(
|
|
160
|
+
Object.keys(curValue).map((key) => {
|
|
161
|
+
if (nextPaths.length === 0) {
|
|
162
|
+
return concatPath(parentPath, key);
|
|
163
|
+
}
|
|
164
|
+
return findMatchPaths(curValue[key], `${nextPaths.join(DIVIDER)}`, withEmptyValue).map(
|
|
165
|
+
(p) => concatPath(parentPath, key, p)
|
|
166
|
+
);
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
// 找不到对应 key 则不匹配
|
|
171
|
+
if (!(curKey in curValue) && !withEmptyValue) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
curValue = curValue[curKey!];
|
|
175
|
+
curPaths.push(curKey);
|
|
176
|
+
curKey = nextPaths.shift();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return [pattern];
|
|
180
|
+
|
|
181
|
+
// const parts = splitPattern(pattern);
|
|
182
|
+
//
|
|
183
|
+
// let prePaths: string[] = [''];
|
|
184
|
+
// let curPath: string = '';
|
|
185
|
+
//
|
|
186
|
+
// for (let i in parts) {
|
|
187
|
+
// const part = parts[i];
|
|
188
|
+
// if (part === ALL) {
|
|
189
|
+
// prePaths = getSubPaths(
|
|
190
|
+
// prePaths.map(p => concatPath(p, curPath)),
|
|
191
|
+
// obj,
|
|
192
|
+
// );
|
|
193
|
+
// curPath = '';
|
|
194
|
+
// } else {
|
|
195
|
+
// curPath = part;
|
|
196
|
+
//
|
|
197
|
+
// /**
|
|
198
|
+
// * 过滤掉后续path 值不存在的prePath
|
|
199
|
+
// * 为什么: prePaths 是返回前一个通配符下所有的路径,但每个路径下的数据的field 可能不同
|
|
200
|
+
// * 这会导致一些prePath 不存在后面所需的路径。如以下场景
|
|
201
|
+
// * const obj = {
|
|
202
|
+
// * a: { b: { c: 1 } },
|
|
203
|
+
// * x: { y: { z: 2 } },
|
|
204
|
+
// * };
|
|
205
|
+
// * expect(Glob.findMatchPaths(obj, '*.y')).toEqual(['x.y']);
|
|
206
|
+
// */
|
|
207
|
+
//
|
|
208
|
+
// prePaths = prePaths.filter(p => {
|
|
209
|
+
// const preValue = p ? get(obj, p) : obj;
|
|
210
|
+
// if (typeof preValue === 'object') {
|
|
211
|
+
// return curPath in preValue;
|
|
212
|
+
// }
|
|
213
|
+
// return true;
|
|
214
|
+
// });
|
|
215
|
+
// }
|
|
216
|
+
// }
|
|
217
|
+
//
|
|
218
|
+
// if (curPath) {
|
|
219
|
+
// return prePaths.map(p => [p, curPath].join(DIVIDER));
|
|
220
|
+
// }
|
|
221
|
+
// return prePaths;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Find all paths matched pattern in object, including paths whoes value is undefined.
|
|
226
|
+
* @param obj
|
|
227
|
+
* @param pattern
|
|
228
|
+
*/
|
|
229
|
+
export function findMatchPathsWithEmptyValue(obj: any, pattern: string): string[] {
|
|
230
|
+
if (!pattern.includes('*')) {
|
|
231
|
+
return [pattern];
|
|
232
|
+
}
|
|
233
|
+
return findMatchPaths(obj, pattern, true);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// export function findMatchPathsWithEmptyValue(obj: any, pattern: string) {
|
|
237
|
+
// const parts = splitPattern(pattern);
|
|
238
|
+
//
|
|
239
|
+
// let prePaths: string[] = [''];
|
|
240
|
+
// let curPath: string = '';
|
|
241
|
+
//
|
|
242
|
+
// for (let i in parts) {
|
|
243
|
+
// const part = parts[i];
|
|
244
|
+
// if (part === ALL) {
|
|
245
|
+
// prePaths = getSubPaths(
|
|
246
|
+
// prePaths.map(p => concatPath(p, curPath)),
|
|
247
|
+
// obj,
|
|
248
|
+
// );
|
|
249
|
+
// curPath = '';
|
|
250
|
+
// } else {
|
|
251
|
+
// curPath = part;
|
|
252
|
+
//
|
|
253
|
+
// /**
|
|
254
|
+
// * 过滤掉后续path 值不存在的prePath
|
|
255
|
+
// * 为什么: prePaths 是返回前一个通配符下所有的路径,但每个路径下的数据的field 可能不同
|
|
256
|
+
// * 这会导致一些prePath 不存在后面所需的路径。如以下场景
|
|
257
|
+
// * const obj = {
|
|
258
|
+
// * a: { b: { c: 1 } },
|
|
259
|
+
// * x: { y: { z: 2 } },
|
|
260
|
+
// * };
|
|
261
|
+
// * expect(Glob.findMatchPaths(obj, '*.y')).toEqual(['x.y']);
|
|
262
|
+
// */
|
|
263
|
+
//
|
|
264
|
+
// // prePaths = prePaths.filter(p => {
|
|
265
|
+
// // const preValue = p ? get(obj, p) : obj;
|
|
266
|
+
// // if (typeof preValue === 'object') {
|
|
267
|
+
// // return curPath in preValue;
|
|
268
|
+
// // }
|
|
269
|
+
// // return true;
|
|
270
|
+
// // });
|
|
271
|
+
// }
|
|
272
|
+
// }
|
|
273
|
+
//
|
|
274
|
+
// if (curPath) {
|
|
275
|
+
// return prePaths.map(p => [p, curPath].join(DIVIDER));
|
|
276
|
+
// }
|
|
277
|
+
// return prePaths;
|
|
278
|
+
// }
|
|
279
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { clone, toPath } from 'lodash-es';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* These functions are copied from Formik.
|
|
10
|
+
* @see https://github.com/jaredpalmer/formik
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const isEmptyArray = (value?: any) => Array.isArray(value) && value.length === 0;
|
|
14
|
+
|
|
15
|
+
/** @private is the given object a Function? */
|
|
16
|
+
export const isFunction = (obj: any): obj is Function => typeof obj === 'function';
|
|
17
|
+
|
|
18
|
+
/** @private is the given object an Object? */
|
|
19
|
+
export const isObject = (obj: any): obj is Object => obj !== null && typeof obj === 'object';
|
|
20
|
+
|
|
21
|
+
/** @private is the given object an integer? */
|
|
22
|
+
export const isInteger = (obj: any): boolean => String(Math.floor(Number(obj))) === obj;
|
|
23
|
+
|
|
24
|
+
/** @private is the given object a string? */
|
|
25
|
+
export const isString = (obj: any): obj is string =>
|
|
26
|
+
Object.prototype.toString.call(obj) === '[object String]';
|
|
27
|
+
|
|
28
|
+
/** @private is the given object a NaN? */
|
|
29
|
+
// eslint-disable-next-line no-self-compare
|
|
30
|
+
export const isNaN = (obj: any): boolean => obj !== obj;
|
|
31
|
+
|
|
32
|
+
/** @private is the given object/value a promise? */
|
|
33
|
+
export const isPromise = (value: any): value is PromiseLike<any> =>
|
|
34
|
+
isObject(value) && isFunction(value.then);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Deeply get a value from an object via its path.
|
|
38
|
+
*/
|
|
39
|
+
export function getIn(obj: any, key: string | string[], def?: any, p: number = 0) {
|
|
40
|
+
const path = toPath(key);
|
|
41
|
+
while (obj && p < path.length) {
|
|
42
|
+
obj = obj[path[p++]];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// check if path is not in the end
|
|
46
|
+
if (p !== path.length && !obj) {
|
|
47
|
+
return def;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return obj === undefined ? def : obj;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Deeply set a value from in object via its path. If the value at `path`
|
|
55
|
+
* has changed, return a shallow copy of obj with `value` set at `path`.
|
|
56
|
+
* If `value` has not changed, return the original `obj`.
|
|
57
|
+
*
|
|
58
|
+
* Existing objects / arrays along `path` are also shallow copied. Sibling
|
|
59
|
+
* objects along path retain the same internal js reference. Since new
|
|
60
|
+
* objects / arrays are only created along `path`, we can test if anything
|
|
61
|
+
* changed in a nested structure by comparing the object's reference in
|
|
62
|
+
* the old and new object, similar to how russian doll cache invalidation
|
|
63
|
+
* works.
|
|
64
|
+
*/
|
|
65
|
+
export function shallowSetIn(obj: any, path: string, value: any): any {
|
|
66
|
+
let res: any = clone(obj); // this keeps inheritance when obj is a class
|
|
67
|
+
let resVal: any = res;
|
|
68
|
+
let i = 0;
|
|
69
|
+
let pathArray = toPath(path);
|
|
70
|
+
|
|
71
|
+
for (; i < pathArray.length - 1; i++) {
|
|
72
|
+
const currentPath: string = pathArray[i];
|
|
73
|
+
let currentObj: any = getIn(obj, pathArray.slice(0, i + 1));
|
|
74
|
+
|
|
75
|
+
if (currentObj && (isObject(currentObj) || Array.isArray(currentObj))) {
|
|
76
|
+
resVal = resVal[currentPath] = clone(currentObj);
|
|
77
|
+
} else {
|
|
78
|
+
const nextPath: string = pathArray[i + 1];
|
|
79
|
+
resVal = resVal[currentPath] = isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Return original object if new value is the same as current
|
|
84
|
+
// `pathArray[i] in obj` is to supoort set undefined value with unknown key
|
|
85
|
+
if ((i === 0 ? obj : resVal)[pathArray[i]] === value && pathArray[i] in obj) {
|
|
86
|
+
return obj;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* In Formik, they delete the key if the value is undefined. but here we keep the key with the undefined value.
|
|
91
|
+
* The reason that Formik tackle in this way is to fix the issue https://github.com/jaredpalmer/formik/issues/727
|
|
92
|
+
* Their fix is https://github.com/jaredpalmer/formik/issues/727, and we roll back to the code before this PR.
|
|
93
|
+
*/
|
|
94
|
+
resVal[pathArray[i]] = value;
|
|
95
|
+
return res;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function keepValidKeys(obj: Record<string, any>, validKeys: string[]) {
|
|
99
|
+
const validKeysSet = new Set(validKeys);
|
|
100
|
+
const newObj: Record<string, any> = {};
|
|
101
|
+
Object.keys(obj).forEach((key) => {
|
|
102
|
+
if (validKeysSet.has(key)) {
|
|
103
|
+
newObj[key] = obj[key];
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return newObj;
|
|
107
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Errors,
|
|
8
|
+
Feedback,
|
|
9
|
+
FeedbackLevel,
|
|
10
|
+
FieldError,
|
|
11
|
+
FieldName,
|
|
12
|
+
FieldWarning,
|
|
13
|
+
FormErrorOptions,
|
|
14
|
+
FormWarningOptions,
|
|
15
|
+
} from '../types';
|
|
16
|
+
|
|
17
|
+
export function toFeedback(
|
|
18
|
+
result: string | FormErrorOptions | FormWarningOptions | undefined,
|
|
19
|
+
name: FieldName
|
|
20
|
+
): FieldError | FieldWarning | undefined {
|
|
21
|
+
if (typeof result === 'string') {
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
message: result,
|
|
25
|
+
level: FeedbackLevel.Error,
|
|
26
|
+
};
|
|
27
|
+
} else if (result?.message) {
|
|
28
|
+
return {
|
|
29
|
+
...result,
|
|
30
|
+
name,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function feedbackToFieldErrorsOrWarnings<T>(name: string, feedback?: Feedback<any>) {
|
|
36
|
+
return {
|
|
37
|
+
[name]: feedback ? [feedback] : [],
|
|
38
|
+
} as T;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const hasError = (errors: Errors) =>
|
|
42
|
+
Object.keys(errors).some((key) => errors[key]?.length > 0);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { InjectionKey } from 'vue';
|
|
7
|
+
|
|
8
|
+
import type { FieldModel } from '../core/field-model';
|
|
9
|
+
import type { FormModel } from '../core/form-model';
|
|
10
|
+
|
|
11
|
+
export const FormModelKey: InjectionKey<FormModel> = Symbol('FormModel');
|
|
12
|
+
export const FieldModelKey: InjectionKey<FieldModel> = Symbol('FieldModel');
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { defineComponent, h, onBeforeUnmount, provide, type PropType, type VNode } from 'vue';
|
|
7
|
+
import { isFunction } from 'lodash-es';
|
|
8
|
+
import { DisposableCollection, useRefresh } from '@flowgram-vue/utils';
|
|
9
|
+
import { useReadonlyReactiveState } from '@flowgram-vue/reactive';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
FieldArrayRenderProps,
|
|
13
|
+
FieldModelState,
|
|
14
|
+
FieldName,
|
|
15
|
+
FieldValue,
|
|
16
|
+
} from '../types/field';
|
|
17
|
+
import { FormModelState } from '../types';
|
|
18
|
+
import { toFieldArray } from '../core/to-field-array';
|
|
19
|
+
import { FieldArrayModel } from '../core/field-array-model';
|
|
20
|
+
import { toFieldState, toFormState } from '../core';
|
|
21
|
+
import { useFormModel } from '../composables/use-form-model';
|
|
22
|
+
import { FieldModelKey } from './context';
|
|
23
|
+
|
|
24
|
+
export default defineComponent({
|
|
25
|
+
name: 'FieldArray',
|
|
26
|
+
props: {
|
|
27
|
+
name: { type: String, required: true },
|
|
28
|
+
defaultValue: { type: Array, default: undefined },
|
|
29
|
+
render: {
|
|
30
|
+
type: Function as PropType<(props: FieldArrayRenderProps<any>) => VNode>,
|
|
31
|
+
default: undefined,
|
|
32
|
+
},
|
|
33
|
+
deps: { type: Array as PropType<FieldName[]>, default: undefined },
|
|
34
|
+
},
|
|
35
|
+
setup(props, { slots }) {
|
|
36
|
+
const formModel = useFormModel();
|
|
37
|
+
const fieldModel =
|
|
38
|
+
formModel.getField<FieldArrayModel<FieldValue>>(props.name) ||
|
|
39
|
+
(formModel.createFieldArray(props.name) as FieldArrayModel<any>);
|
|
40
|
+
|
|
41
|
+
const field = toFieldArray(fieldModel);
|
|
42
|
+
const refresh = useRefresh();
|
|
43
|
+
|
|
44
|
+
const fieldModelState = useReadonlyReactiveState<FieldModelState>(fieldModel.reactiveState);
|
|
45
|
+
const formModelState = useReadonlyReactiveState<FormModelState>(formModel.reactiveState);
|
|
46
|
+
const fieldState = toFieldState(fieldModelState);
|
|
47
|
+
const formState = toFormState(formModelState);
|
|
48
|
+
|
|
49
|
+
const bind = () => {
|
|
50
|
+
if (fieldModel.disposed) {
|
|
51
|
+
refresh();
|
|
52
|
+
return () => {};
|
|
53
|
+
}
|
|
54
|
+
fieldModel.renderCount = fieldModel.renderCount + 1;
|
|
55
|
+
|
|
56
|
+
if (!formModel.getValueIn(props.name) !== undefined && props.defaultValue !== undefined) {
|
|
57
|
+
formModel.setInitValueIn(props.name, props.defaultValue);
|
|
58
|
+
refresh();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const disposableCollection = new DisposableCollection();
|
|
62
|
+
|
|
63
|
+
disposableCollection.push(
|
|
64
|
+
fieldModel.onValueChange(() => {
|
|
65
|
+
refresh();
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (props.deps) {
|
|
70
|
+
props.deps.forEach((dep) => {
|
|
71
|
+
const disposable = formModel.getField(dep)?.onValueChange(() => {
|
|
72
|
+
refresh();
|
|
73
|
+
});
|
|
74
|
+
if (disposable) {
|
|
75
|
+
disposableCollection.push(disposable);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
disposableCollection.dispose();
|
|
82
|
+
|
|
83
|
+
if (fieldModel.renderCount > 1) {
|
|
84
|
+
fieldModel.renderCount = fieldModel.renderCount - 1;
|
|
85
|
+
} else {
|
|
86
|
+
const newFieldModel = formModel.getField(fieldModel.name);
|
|
87
|
+
if (newFieldModel === fieldModel) fieldModel.dispose();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const unbind = bind();
|
|
93
|
+
onBeforeUnmount(unbind);
|
|
94
|
+
|
|
95
|
+
provide(FieldModelKey, fieldModel);
|
|
96
|
+
|
|
97
|
+
return () => {
|
|
98
|
+
if (fieldModel.disposed) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const slotProps: FieldArrayRenderProps<any> = { field, fieldState, formState };
|
|
103
|
+
|
|
104
|
+
if (props.render && isFunction(props.render)) {
|
|
105
|
+
return props.render(slotProps);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const slot = slots.default;
|
|
109
|
+
if (isFunction(slot)) {
|
|
110
|
+
return slot(slotProps);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return h('span', 'Invalid Array render');
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
</script>
|