@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.
Files changed (47) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +0 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +0 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +65 -0
  8. package/src/composables/index.ts +13 -0
  9. package/src/composables/use-current-field-state.ts +29 -0
  10. package/src/composables/use-current-field.ts +31 -0
  11. package/src/composables/use-field-validate.ts +25 -0
  12. package/src/composables/use-field.ts +47 -0
  13. package/src/composables/use-form-model.ts +17 -0
  14. package/src/composables/use-form-state.ts +25 -0
  15. package/src/composables/use-form.ts +16 -0
  16. package/src/composables/use-watch.ts +34 -0
  17. package/src/constants.ts +34 -0
  18. package/src/core/create-form.ts +58 -0
  19. package/src/core/field-array-model.ts +333 -0
  20. package/src/core/field-model.ts +396 -0
  21. package/src/core/form-model.ts +362 -0
  22. package/src/core/index.ts +14 -0
  23. package/src/core/path.ts +125 -0
  24. package/src/core/store.ts +33 -0
  25. package/src/core/to-field-array.ts +52 -0
  26. package/src/core/to-field.ts +80 -0
  27. package/src/core/to-form.ts +64 -0
  28. package/src/core/utils.ts +123 -0
  29. package/src/env.d.ts +10 -0
  30. package/src/index.ts +30 -0
  31. package/src/types/common.ts +6 -0
  32. package/src/types/field.ts +193 -0
  33. package/src/types/form.ts +145 -0
  34. package/src/types/index.ts +8 -0
  35. package/src/types/validate.ts +88 -0
  36. package/src/utils/dom.ts +31 -0
  37. package/src/utils/event.ts +32 -0
  38. package/src/utils/glob.ts +279 -0
  39. package/src/utils/index.ts +8 -0
  40. package/src/utils/object.ts +107 -0
  41. package/src/utils/validate.ts +42 -0
  42. package/src/vue/context.ts +12 -0
  43. package/src/vue/field-array.vue +117 -0
  44. package/src/vue/field.vue +127 -0
  45. package/src/vue/form.vue +61 -0
  46. package/src/vue/index.ts +11 -0
  47. 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,8 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './object';
7
+ export * from './dom';
8
+ export * from './glob';
@@ -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>