@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,396 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { nanoid } from 'nanoid';
7
+ import { get, groupBy, some } from 'lodash-es';
8
+ import { Disposable, DisposableCollection, Emitter } from '@flowgram-vue/utils';
9
+ import { ReactiveState } from '@flowgram-vue/reactive';
10
+
11
+ import { toFeedback } from '../utils/validate';
12
+ import { FieldModelState, FieldName, FieldValue, Ref } from '../types/field';
13
+ import {
14
+ Errors,
15
+ FeedbackLevel,
16
+ FieldError,
17
+ FieldWarning,
18
+ Validate,
19
+ ValidateTrigger,
20
+ Warnings,
21
+ } from '../types';
22
+ import { createFieldModelState, DEFAULT_FIELD_STATE } from '../constants';
23
+ import {
24
+ clearFeedbacks,
25
+ FieldEventUtils,
26
+ mergeFeedbacks,
27
+ shouldValidate,
28
+ updateFeedbacksName,
29
+ } from './utils';
30
+ import { Path } from './path';
31
+ import { FormModel } from './form-model';
32
+
33
+ interface OnValueChangePayload<TValue> {
34
+ value: TValue | undefined;
35
+ prevValue: TValue | undefined;
36
+ formValues: any;
37
+ prevFormValues: any;
38
+ }
39
+
40
+ export class FieldModel<TValue extends FieldValue = FieldValue> implements Disposable {
41
+ readonly onValueChangeEmitter = new Emitter<OnValueChangePayload<TValue>>();
42
+
43
+ readonly form: FormModel;
44
+
45
+ readonly id: string;
46
+
47
+ readonly onValueChange = this.onValueChangeEmitter.event;
48
+
49
+ protected toDispose = new DisposableCollection();
50
+
51
+ protected _ref?: Ref;
52
+
53
+ protected _path: Path;
54
+
55
+ protected _state: ReactiveState<FieldModelState> = new ReactiveState<FieldModelState>(
56
+ createFieldModelState()
57
+ );
58
+
59
+ /**
60
+ * @deprecated
61
+ * 原用于直接给field 设置validate 逻辑,现将该逻辑放到form._options.validate 中设置,该字段暂时弃用
62
+ */
63
+ originalValidate?: Validate;
64
+
65
+ protected _renderCount: number = 0;
66
+
67
+ constructor(path: Path, form: FormModel) {
68
+ this._path = path;
69
+ this.form = form;
70
+ this.id = nanoid();
71
+
72
+ const changeDisposable = this.form.onFormValuesChange((payload) => {
73
+ const { values, prevValues } = payload;
74
+ if (FieldEventUtils.shouldTriggerFieldChangeEvent(payload, this.name)) {
75
+ this.onValueChangeEmitter.fire({
76
+ value: get(values, this.name),
77
+ prevValue: get(prevValues, this.name),
78
+ formValues: values,
79
+ prevFormValues: prevValues,
80
+ });
81
+ if (
82
+ shouldValidate(ValidateTrigger.onChange, this.form.validationTrigger) &&
83
+ FieldEventUtils.shouldTriggerFieldValidateWhenChange(payload, this.name)
84
+ ) {
85
+ this.validate();
86
+ }
87
+ }
88
+ });
89
+ this.toDispose.push(changeDisposable);
90
+
91
+ // if (shouldValidate(ValidateTrigger.onChange, this.form.validationTrigger)) {
92
+ // const validateDisposable = this.form.onFormValuesChange(({ name, values, prevValues }) => {
93
+ // /**
94
+ // * Field 值变更时,所有 ancestor 以及所有child 和 grand child 的校验都要触发
95
+ // */
96
+ // if (Glob.isMatchOrParent(this.name, name) || Glob.isMatchOrParent(name, this.name)) {
97
+ // this.validate();
98
+ // }
99
+ // });
100
+ // this.toDispose.push(validateDisposable);
101
+ // }
102
+
103
+ this.toDispose.push(this.onValueChangeEmitter);
104
+
105
+ this.initState();
106
+ }
107
+
108
+ protected _mount: boolean = false;
109
+
110
+ get renderCount() {
111
+ return this._renderCount;
112
+ }
113
+
114
+ set renderCount(n: number) {
115
+ this._renderCount = n;
116
+ }
117
+
118
+ private initState() {
119
+ const initialErrors = get(this.form.state.errors, this.name);
120
+ const initialWarnings = get(this.form.state.warnings, this.name);
121
+
122
+ if (initialErrors) {
123
+ this.state.errors = {
124
+ [this.name]: initialErrors,
125
+ };
126
+ }
127
+ if (initialWarnings) {
128
+ this.state.warnings = {
129
+ [this.name]: initialWarnings,
130
+ };
131
+ }
132
+ }
133
+
134
+ get path() {
135
+ return this._path;
136
+ }
137
+
138
+ get name() {
139
+ return this._path.toString();
140
+ }
141
+
142
+ set name(name: FieldName) {
143
+ this._path = new Path(name);
144
+ }
145
+
146
+ get ref() {
147
+ return this._ref;
148
+ }
149
+
150
+ set ref(ref: Ref | undefined) {
151
+ this._ref = ref;
152
+ }
153
+
154
+ get state() {
155
+ return this._state.value;
156
+ }
157
+
158
+ get reactiveState() {
159
+ return this._state;
160
+ }
161
+
162
+ get value() {
163
+ return this.form.getValueIn(this.name);
164
+ }
165
+
166
+ set value(value: TValue | undefined) {
167
+ this.form.setValueIn(this.name, value);
168
+ if (!this.state.isTouched) {
169
+ this.state.isTouched = true;
170
+ this.bubbleState();
171
+ }
172
+ }
173
+
174
+ updateNameForLeafState(newName: string) {
175
+ const { errors, warnings } = this.state;
176
+ const nameInErrors = errors ? Object.keys(errors)?.[0] : undefined;
177
+ if (nameInErrors && errors?.[nameInErrors] && nameInErrors !== newName) {
178
+ this.state.errors = {
179
+ [newName]: errors?.[nameInErrors]
180
+ ? updateFeedbacksName(errors?.[nameInErrors], newName)
181
+ : errors?.[nameInErrors],
182
+ };
183
+ }
184
+ const nameInWarnings = warnings ? Object.keys(warnings)?.[0] : undefined;
185
+ if (nameInWarnings && warnings?.[nameInWarnings] && nameInWarnings !== newName) {
186
+ this.state.warnings = {
187
+ [newName]: warnings?.[nameInWarnings]
188
+ ? updateFeedbacksName(warnings?.[nameInWarnings], newName)
189
+ : warnings?.[nameInWarnings],
190
+ };
191
+ }
192
+ }
193
+
194
+ // recursiveUpdateName(name: FieldName) {
195
+ // if (this.children?.length) {
196
+ // this.children.forEach(c => {
197
+ // c.recursiveUpdateName(c.path.replaceParent(this.path, new Path(name)).toString());
198
+ // });
199
+ // } else {
200
+ // this.updateNameForLeafState(name);
201
+ // this.bubbleState();
202
+ // }
203
+ // this.name = name;
204
+ // }
205
+
206
+ /**
207
+ * @deprecated
208
+ * @param validate
209
+ * @param from
210
+ */
211
+ updateValidate(validate: Validate | undefined, from?: 'ui') {
212
+ if (from === 'ui') {
213
+ // todo(heyuan):暂时逻辑: 只在没有全局配置校验时来自ui 的validate 才生效。 后续需要支持多validate合并, ui 和全局的都需要生效
214
+ if (!this.originalValidate) {
215
+ this.originalValidate = validate;
216
+ }
217
+ } else {
218
+ this.originalValidate = validate;
219
+ }
220
+ }
221
+
222
+ bubbleState() {
223
+ const { errors, warnings } = this.state;
224
+
225
+ if (this.parent) {
226
+ this.parent.state.isTouched = some(
227
+ this.parent.children.map((c) => c.state.isTouched),
228
+ Boolean
229
+ );
230
+ this.parent.state.invalid = some(
231
+ this.parent.children.map((c) => c.state.invalid),
232
+ Boolean
233
+ );
234
+ this.parent.state.isDirty = some(
235
+ this.parent.children.map((c) => c.state.isDirty),
236
+ Boolean
237
+ );
238
+ this.parent.state.isValidating = some(
239
+ this.parent.children.map((c) => c.state.isValidating),
240
+ Boolean
241
+ );
242
+ this.parent.state.errors = errors
243
+ ? mergeFeedbacks<Errors>(this.parent.state.errors, errors)
244
+ : clearFeedbacks(this.name, this.parent.state.errors);
245
+ this.parent.state.warnings = warnings
246
+ ? mergeFeedbacks<Warnings>(this.parent.state.warnings, warnings)
247
+ : clearFeedbacks(this.name, this.parent.state.warnings);
248
+
249
+ this.parent.bubbleState();
250
+ return;
251
+ }
252
+ // parent 不存在,则更新form state
253
+ this.form.state.isTouched = some(
254
+ this.form.fields.map((f) => f.state.isTouched),
255
+ Boolean
256
+ );
257
+ this.form.state.invalid = some(
258
+ this.form.fields.map((f) => f.state.invalid),
259
+ Boolean
260
+ );
261
+ this.form.state.isDirty = some(
262
+ this.form.fields.map((f) => f.state.isDirty),
263
+ Boolean
264
+ );
265
+ this.form.state.isValidating = some(
266
+ this.form.fields.map((f) => f.state.isValidating),
267
+ Boolean
268
+ );
269
+ this.form.state.errors = errors
270
+ ? mergeFeedbacks<Errors>(this.form.state.errors, errors)
271
+ : clearFeedbacks(this.name, this.form.state.errors);
272
+ this.form.state.warnings = warnings
273
+ ? mergeFeedbacks<Warnings>(this.form.state.warnings, warnings)
274
+ : clearFeedbacks(this.name, this.form.state.warnings);
275
+ // console.log('>>>> bubble state: ', this.form.state.errors, this.form.state.invalid, this.form.fields.map(f => f.state.invalid))
276
+ }
277
+
278
+ clearState() {
279
+ this.state.errors = DEFAULT_FIELD_STATE.errors;
280
+ this.state.warnings = DEFAULT_FIELD_STATE.warnings;
281
+ this.state.isTouched = DEFAULT_FIELD_STATE.isTouched;
282
+ this.state.isDirty = DEFAULT_FIELD_STATE.isDirty;
283
+ this.bubbleState();
284
+ }
285
+
286
+ get children(): FieldModel[] {
287
+ const res: FieldModel[] = [];
288
+ this.form.fieldMap.forEach((field, path: string) => {
289
+ if (this.path.isChild(path)) {
290
+ res.push(field);
291
+ }
292
+ });
293
+ return res;
294
+ }
295
+
296
+ get parent(): FieldModel | undefined {
297
+ const parentPath = this.path.parent;
298
+ if (!parentPath) {
299
+ return undefined;
300
+ }
301
+ return this.form.fieldMap.get(parentPath.toString());
302
+ }
303
+
304
+ clear() {
305
+ if (!this.value) {
306
+ return;
307
+ }
308
+ this.value = undefined;
309
+ }
310
+
311
+ async validate() {
312
+ // 以下代码由于导致arr 配置的校验不触发,暂时注释,支持对父节点配置校验逻辑
313
+ // const children = this.children;
314
+
315
+ // 如果是非叶子field, 执行children的校验。暂不支持在父级上配校验器
316
+ // if (children?.length) {
317
+ // await Promise.all(this.children.map(c => c.validate()));
318
+ // return;
319
+ // }
320
+ await this.validateSelf();
321
+ }
322
+
323
+ async validateSelf() {
324
+ this.state.isValidating = true;
325
+ this.bubbleState();
326
+ const { errors, warnings } = await this._runAsyncValidate();
327
+
328
+ if (errors?.length) {
329
+ this.state.errors = groupBy(errors, 'name');
330
+ this.state.invalid = true;
331
+ } else {
332
+ this.state.errors = { [this.name]: [] };
333
+ this.state.invalid = false;
334
+ }
335
+
336
+ if (warnings?.length) {
337
+ this.state.warnings = groupBy(warnings, 'name');
338
+ } else {
339
+ this.state.warnings = { [this.name]: [] };
340
+ }
341
+
342
+ this.state.isValidating = false;
343
+ this.bubbleState();
344
+ this.form.onValidateEmitter.fire(this.form.state);
345
+ }
346
+
347
+ protected async _runAsyncValidate(): Promise<{
348
+ errors?: FieldError[];
349
+ warnings?: FieldWarning[];
350
+ }> {
351
+ let errors: FieldError[] = [];
352
+ let warnings: FieldWarning[] = [];
353
+
354
+ const results = await this.form.validateIn(this.name);
355
+ if (!results?.length) {
356
+ return {};
357
+ } else {
358
+ const feedbacks = results.map((result) => toFeedback(result, this.name)).filter(Boolean) as (
359
+ | FieldError
360
+ | FieldWarning
361
+ )[];
362
+
363
+ if (!feedbacks?.length) {
364
+ return {};
365
+ }
366
+
367
+ const groupedFeedbacks = groupBy(feedbacks, 'level');
368
+
369
+ warnings = warnings.concat((groupedFeedbacks[FeedbackLevel.Warning] as FieldWarning[]) || []);
370
+ errors = errors.concat((groupedFeedbacks[FeedbackLevel.Error] as FieldError[]) || []);
371
+ }
372
+
373
+ return { errors, warnings };
374
+ }
375
+
376
+ updateState(s: Partial<FieldModel>) {
377
+ // todo
378
+ }
379
+
380
+ dispose() {
381
+ this.children.map((c) => c.dispose());
382
+ // Do not reset state when field disposed, since it will clear errors and warnings in form model as well.
383
+ // todo: remove following line and related ut after a few weeks test online
384
+ // this.clearState();
385
+ this.toDispose.dispose();
386
+ this.form.fieldMap.delete(this.path.toString());
387
+ }
388
+
389
+ onDispose(fn: () => void) {
390
+ this.toDispose.onDispose(fn);
391
+ }
392
+
393
+ get disposed() {
394
+ return this.toDispose.disposed;
395
+ }
396
+ }