@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,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
|
+
}
|