@flowgram-vue/node 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 +70 -0
- package/src/NodeFormRender.vue +35 -0
- package/src/env.d.ts +10 -0
- package/src/form-model-v2.ts +575 -0
- package/src/form-plugin.ts +129 -0
- package/src/form-render.ts +13 -0
- package/src/get-node-form.ts +109 -0
- package/src/helpers.ts +29 -0
- package/src/hooks.ts +98 -0
- package/src/index.ts +11 -0
- package/src/types.ts +153 -0
- package/src/utils.ts +86 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { get, groupBy, isEmpty, isNil, mapKeys, uniq } from 'lodash-es';
|
|
7
|
+
import { Disposable, DisposableCollection, Emitter } from '@flowgram-vue/utils';
|
|
8
|
+
import {
|
|
9
|
+
FlowNodeFormData,
|
|
10
|
+
FormFeedback,
|
|
11
|
+
FormItem,
|
|
12
|
+
FormManager,
|
|
13
|
+
FormModel,
|
|
14
|
+
FormModelValid,
|
|
15
|
+
IFormItem,
|
|
16
|
+
NodeFormContext,
|
|
17
|
+
OnFormValuesChangePayload,
|
|
18
|
+
} from '@flowgram-vue/form-core';
|
|
19
|
+
import {
|
|
20
|
+
createForm,
|
|
21
|
+
FieldArrayModel,
|
|
22
|
+
FieldName,
|
|
23
|
+
FieldValue,
|
|
24
|
+
type FormControl,
|
|
25
|
+
FormModel as NativeFormModel,
|
|
26
|
+
FormValidateReturn,
|
|
27
|
+
Glob,
|
|
28
|
+
IField,
|
|
29
|
+
IFieldArray,
|
|
30
|
+
toForm,
|
|
31
|
+
} from '@flowgram-vue/form';
|
|
32
|
+
import { FlowNodeEntity } from '@flowgram-vue/document';
|
|
33
|
+
import { PlaygroundContext, PluginContext } from '@flowgram-vue/core';
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
convertGlobPath,
|
|
37
|
+
findMatchedInMap,
|
|
38
|
+
formFeedbacksToNodeCoreFormFeedbacks,
|
|
39
|
+
mergeEffectReturn,
|
|
40
|
+
runAndDeleteEffectReturn,
|
|
41
|
+
} from './utils';
|
|
42
|
+
import {
|
|
43
|
+
DataEvent,
|
|
44
|
+
Effect,
|
|
45
|
+
EffectOptions,
|
|
46
|
+
EffectReturn,
|
|
47
|
+
FormMeta,
|
|
48
|
+
onFormValueChangeInPayload,
|
|
49
|
+
} from './types';
|
|
50
|
+
import { renderForm } from './form-render';
|
|
51
|
+
import { FormPlugin } from './form-plugin';
|
|
52
|
+
|
|
53
|
+
const DEFAULT = {
|
|
54
|
+
// Different formModel should have different reference
|
|
55
|
+
EFFECT_MAP: () => ({}),
|
|
56
|
+
EFFECT_RETURN_MAP: () =>
|
|
57
|
+
new Map([
|
|
58
|
+
[DataEvent.onValueInitOrChange, {}],
|
|
59
|
+
[DataEvent.onValueChange, {}],
|
|
60
|
+
[DataEvent.onValueInit, {}],
|
|
61
|
+
[DataEvent.onArrayAppend, {}],
|
|
62
|
+
[DataEvent.onArrayDelete, {}],
|
|
63
|
+
]),
|
|
64
|
+
FORM_FEEDBACKS: () => [],
|
|
65
|
+
VALID: null,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export class FormModelV2 extends FormModel implements Disposable {
|
|
69
|
+
protected effectMap: Record<string, EffectOptions[]> = DEFAULT.EFFECT_MAP();
|
|
70
|
+
|
|
71
|
+
protected effectReturnMap: Map<DataEvent, Record<string, EffectReturn>> =
|
|
72
|
+
DEFAULT.EFFECT_RETURN_MAP();
|
|
73
|
+
|
|
74
|
+
protected plugins: FormPlugin[] = [];
|
|
75
|
+
|
|
76
|
+
protected node: FlowNodeEntity;
|
|
77
|
+
|
|
78
|
+
protected formFeedbacks: FormValidateReturn | undefined = DEFAULT.FORM_FEEDBACKS();
|
|
79
|
+
|
|
80
|
+
protected onInitializedEmitter = new Emitter<FormModel>();
|
|
81
|
+
|
|
82
|
+
protected onValidateEmitter = new Emitter<FormModel>();
|
|
83
|
+
|
|
84
|
+
readonly onValidate = this.onValidateEmitter.event;
|
|
85
|
+
|
|
86
|
+
readonly onInitialized = this.onInitializedEmitter.event;
|
|
87
|
+
|
|
88
|
+
protected onDisposeEmitter = new Emitter<void>();
|
|
89
|
+
|
|
90
|
+
readonly onDispose = this.onDisposeEmitter.event;
|
|
91
|
+
|
|
92
|
+
protected toDispose = new DisposableCollection();
|
|
93
|
+
|
|
94
|
+
protected onFormValuesChangeEmitter = new Emitter<OnFormValuesChangePayload>();
|
|
95
|
+
|
|
96
|
+
readonly onFormValuesChange = this.onFormValuesChangeEmitter.event;
|
|
97
|
+
|
|
98
|
+
protected onValidChangeEmitter = new Emitter<FormModelValid>();
|
|
99
|
+
|
|
100
|
+
readonly onValidChange = this.onValidChangeEmitter.event;
|
|
101
|
+
|
|
102
|
+
protected onFeedbacksChangeEmitter = new Emitter<FormFeedback[]>();
|
|
103
|
+
|
|
104
|
+
readonly onFeedbacksChange = this.onFeedbacksChangeEmitter.event;
|
|
105
|
+
|
|
106
|
+
constructor(node: FlowNodeEntity) {
|
|
107
|
+
super();
|
|
108
|
+
this.node = node;
|
|
109
|
+
this.toDispose.pushAll([
|
|
110
|
+
this.onInitializedEmitter,
|
|
111
|
+
this.onValidateEmitter,
|
|
112
|
+
this.onValidChangeEmitter,
|
|
113
|
+
this.onFeedbacksChangeEmitter,
|
|
114
|
+
this.onFormValuesChangeEmitter,
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
protected _valid: FormModelValid = DEFAULT.VALID;
|
|
119
|
+
|
|
120
|
+
get valid(): FormModelValid {
|
|
121
|
+
return this._valid;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private set valid(valid: FormModelValid) {
|
|
125
|
+
this._valid = valid;
|
|
126
|
+
this.onValidChangeEmitter.fire(valid);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get flowNodeEntity() {
|
|
130
|
+
return this.node;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get formManager() {
|
|
134
|
+
return this.node.getService(FormManager);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
protected _formControl?: FormControl<any>;
|
|
138
|
+
|
|
139
|
+
get formControl() {
|
|
140
|
+
return this._formControl;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
protected _formMeta: FormMeta;
|
|
144
|
+
|
|
145
|
+
get formMeta(): FormMeta {
|
|
146
|
+
return this._formMeta || (this.node.getNodeRegistry().formMeta as FormMeta);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get values() {
|
|
150
|
+
return this.nativeFormModel?.values;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected _feedbacks: FormFeedback[] = [];
|
|
154
|
+
|
|
155
|
+
get feedbacks(): FormFeedback[] {
|
|
156
|
+
return this._feedbacks;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
updateFormValues(value: any) {
|
|
160
|
+
if (this.nativeFormModel) {
|
|
161
|
+
const finalValue = this.formMeta.formatOnInit
|
|
162
|
+
? this.formMeta.formatOnInit(value, this.nodeContext)
|
|
163
|
+
: value;
|
|
164
|
+
this.nativeFormModel.values = finalValue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private set feedbacks(feedbacks: FormFeedback[]) {
|
|
169
|
+
this._feedbacks = feedbacks;
|
|
170
|
+
this.onFeedbacksChangeEmitter.fire(feedbacks);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get formItemPathMap(): Map<string, IFormItem> {
|
|
174
|
+
return new Map<string, IFormItem>();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
protected _initialized: boolean = false;
|
|
178
|
+
|
|
179
|
+
get initialized(): boolean {
|
|
180
|
+
return this._initialized;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
get nodeContext(): NodeFormContext {
|
|
184
|
+
return {
|
|
185
|
+
node: this.node,
|
|
186
|
+
playgroundContext: this.node.getService(PlaygroundContext),
|
|
187
|
+
clientContext: this.node.getService(PluginContext),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get nativeFormModel(): NativeFormModel | undefined {
|
|
192
|
+
return this._formControl?._formModel;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
render() {
|
|
196
|
+
return renderForm(this);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
initPlugins(plugins: FormPlugin[]) {
|
|
200
|
+
if (!plugins.length) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.plugins = plugins;
|
|
205
|
+
plugins.forEach((plugin) => {
|
|
206
|
+
plugin.init(this);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
init(formMeta: FormMeta, rawInitialValues?: any) {
|
|
211
|
+
/* 透传 onFormValuesChange 事件给 FlowNodeFormData */
|
|
212
|
+
const formData = this.node.getData<FlowNodeFormData>(FlowNodeFormData);
|
|
213
|
+
this.onFormValuesChange(() => {
|
|
214
|
+
this._valid = null;
|
|
215
|
+
formData.fireChange();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
(formMeta.plugins || [])?.forEach((_plugin) => {
|
|
219
|
+
if (_plugin.setupFormMeta) {
|
|
220
|
+
formMeta = _plugin.setupFormMeta(formMeta, this.nodeContext);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
this._formMeta = formMeta;
|
|
225
|
+
|
|
226
|
+
const { validateTrigger, validate, effect } = formMeta;
|
|
227
|
+
if (effect) {
|
|
228
|
+
this.effectMap = effect;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 计算初始值: defaultValues 是默认表单值,不需要被format, 而rawInitialValues 是用户创建form 时传入的初始值,可能不同于表单数据格式,需要被format
|
|
232
|
+
const defaultValues =
|
|
233
|
+
typeof formMeta.defaultValues === 'function'
|
|
234
|
+
? formMeta.defaultValues(this.nodeContext)
|
|
235
|
+
: formMeta.defaultValues;
|
|
236
|
+
|
|
237
|
+
const initialValues = formMeta.formatOnInit
|
|
238
|
+
? formMeta.formatOnInit(rawInitialValues, this.nodeContext)
|
|
239
|
+
: rawInitialValues;
|
|
240
|
+
|
|
241
|
+
// 初始化底层表单
|
|
242
|
+
const { control } = createForm({
|
|
243
|
+
initialValues: initialValues || defaultValues,
|
|
244
|
+
validateTrigger,
|
|
245
|
+
context: this.nodeContext,
|
|
246
|
+
validate: validate,
|
|
247
|
+
disableAutoInit: true,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
this._formControl = control;
|
|
251
|
+
const nativeFormModel = control._formModel;
|
|
252
|
+
this.toDispose.push(nativeFormModel);
|
|
253
|
+
|
|
254
|
+
// forward onFormValuesChange event
|
|
255
|
+
nativeFormModel.onFormValuesChange((props) => {
|
|
256
|
+
this.onFormValuesChangeEmitter.fire(props);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (formMeta.plugins) {
|
|
260
|
+
this.initPlugins(formMeta.plugins);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Form 数据变更时触发对应的effect
|
|
264
|
+
nativeFormModel.onFormValuesChange(({ values, prevValues, name, options }) => {
|
|
265
|
+
Object.keys(this.effectMap).forEach((pattern) => {
|
|
266
|
+
// 找到匹配 pattern 的数据路径
|
|
267
|
+
const paths = uniq([
|
|
268
|
+
...Glob.findMatchPaths(values, pattern),
|
|
269
|
+
...Glob.findMatchPaths(prevValues, pattern),
|
|
270
|
+
]).filter(
|
|
271
|
+
(path) =>
|
|
272
|
+
// trigger effect by compare if value changed
|
|
273
|
+
get(values, path) !== get(prevValues, path)
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (Glob.isMatchOrParent(pattern, name)) {
|
|
277
|
+
const currentName = Glob.getParentPathByPattern(pattern, name);
|
|
278
|
+
if (!paths.includes(currentName)) {
|
|
279
|
+
// trigger effect anyway
|
|
280
|
+
paths.push(currentName);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const effectOptionsArr = this.effectMap[pattern];
|
|
285
|
+
|
|
286
|
+
paths.forEach((path) => {
|
|
287
|
+
let eventList = [DataEvent.onValueChange, DataEvent.onValueInitOrChange];
|
|
288
|
+
const isPrevNil = isNil(get(prevValues, path));
|
|
289
|
+
|
|
290
|
+
if (isPrevNil) {
|
|
291
|
+
// HACK: For array append, onFormValuesInit will auto triggered for array[index]
|
|
292
|
+
if (options?.action === 'array-append' && Glob.isMatch(`${name}.*`, path)) {
|
|
293
|
+
eventList = [];
|
|
294
|
+
} else {
|
|
295
|
+
eventList = [DataEvent.onValueInit, DataEvent.onValueInitOrChange];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 对触发 init 事件的 name 或他的字 path 触发 effect
|
|
300
|
+
runAndDeleteEffectReturn(this.effectReturnMap, path, eventList);
|
|
301
|
+
|
|
302
|
+
// 执行该事件配置下所有 onValueChange 事件的 effect
|
|
303
|
+
effectOptionsArr.forEach(({ effect, event }: EffectOptions) => {
|
|
304
|
+
if (eventList.includes(event)) {
|
|
305
|
+
// 执行 effect
|
|
306
|
+
const effectReturn = (effect as Effect)({
|
|
307
|
+
name: path,
|
|
308
|
+
value: get(values, path),
|
|
309
|
+
prevValue: get(prevValues, path),
|
|
310
|
+
formValues: values,
|
|
311
|
+
form: toForm(this.nativeFormModel!),
|
|
312
|
+
context: this.nodeContext,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// 更新 effect return
|
|
316
|
+
if (
|
|
317
|
+
effectReturn &&
|
|
318
|
+
typeof effectReturn === 'function' &&
|
|
319
|
+
this.effectReturnMap.has(event)
|
|
320
|
+
) {
|
|
321
|
+
const eventMap = this.effectReturnMap.get(event) as Record<string, EffectReturn>;
|
|
322
|
+
eventMap[path] = mergeEffectReturn(eventMap[path], effectReturn);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Form 数据初始化时触发对应的 effect
|
|
331
|
+
nativeFormModel.onFormValuesInit(({ values, name, prevValues }) => {
|
|
332
|
+
Object.keys(this.effectMap).forEach((pattern) => {
|
|
333
|
+
// 找到匹配 pattern 的数据路径
|
|
334
|
+
const paths = Glob.findMatchPaths(values, pattern);
|
|
335
|
+
|
|
336
|
+
// 获取配置在该 pattern上的所有effect配置
|
|
337
|
+
const effectOptionsArr = this.effectMap[pattern];
|
|
338
|
+
|
|
339
|
+
paths.forEach((path) => {
|
|
340
|
+
if (Glob.isMatchOrParent(name, path) || name === path) {
|
|
341
|
+
// 对触发 init 事件的 name 或他的字 path 触发 effect
|
|
342
|
+
runAndDeleteEffectReturn(this.effectReturnMap, path, [
|
|
343
|
+
DataEvent.onValueInit,
|
|
344
|
+
DataEvent.onValueInitOrChange,
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
effectOptionsArr.forEach(({ event, effect }: EffectOptions) => {
|
|
348
|
+
if (event === DataEvent.onValueInit || event === DataEvent.onValueInitOrChange) {
|
|
349
|
+
const effectReturn = (effect as Effect)({
|
|
350
|
+
name: path,
|
|
351
|
+
value: get(values, path),
|
|
352
|
+
formValues: values,
|
|
353
|
+
prevValue: get(prevValues, path),
|
|
354
|
+
form: toForm(this.nativeFormModel!),
|
|
355
|
+
context: this.nodeContext,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// 更新 effect return
|
|
359
|
+
if (
|
|
360
|
+
effectReturn &&
|
|
361
|
+
typeof effectReturn === 'function' &&
|
|
362
|
+
this.effectReturnMap.has(event)
|
|
363
|
+
) {
|
|
364
|
+
const eventMap = this.effectReturnMap.get(event) as Record<string, EffectReturn>;
|
|
365
|
+
eventMap[path] = mergeEffectReturn(eventMap[path], effectReturn);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// 为 Field 添加 effect, 主要针对array
|
|
375
|
+
nativeFormModel.onFieldModelCreate((field) => {
|
|
376
|
+
// register effect
|
|
377
|
+
const effectOptionsArr = findMatchedInMap<EffectOptions[]>(field, this.effectMap);
|
|
378
|
+
if (effectOptionsArr?.length) {
|
|
379
|
+
// 按事件聚合
|
|
380
|
+
const eventMap = groupBy(effectOptionsArr, 'event');
|
|
381
|
+
|
|
382
|
+
mapKeys(eventMap, (optionsArr, event) => {
|
|
383
|
+
const combinedEffect = (props: any) => {
|
|
384
|
+
// 该事件下执行所有effect
|
|
385
|
+
optionsArr.forEach(({ effect }) =>
|
|
386
|
+
effect({
|
|
387
|
+
...props,
|
|
388
|
+
formValues: nativeFormModel.values,
|
|
389
|
+
form: toForm(this.nativeFormModel!),
|
|
390
|
+
context: this.nodeContext,
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
switch (event) {
|
|
396
|
+
case DataEvent.onArrayAppend:
|
|
397
|
+
if (field instanceof FieldArrayModel) {
|
|
398
|
+
(field as FieldArrayModel).onAppend(combinedEffect);
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
case DataEvent.onArrayDelete:
|
|
402
|
+
if (field instanceof FieldArrayModel) {
|
|
403
|
+
(field as FieldArrayModel).onDelete(combinedEffect);
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// 手动初始化form
|
|
412
|
+
this._formControl.init();
|
|
413
|
+
|
|
414
|
+
this._initialized = true;
|
|
415
|
+
|
|
416
|
+
this.onInitializedEmitter.fire(this);
|
|
417
|
+
|
|
418
|
+
this.onDispose(() => {
|
|
419
|
+
this._initialized = false;
|
|
420
|
+
this.effectMap = {};
|
|
421
|
+
nativeFormModel.dispose();
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
toJSON() {
|
|
426
|
+
if (this.formMeta.formatOnSubmit) {
|
|
427
|
+
return this.formMeta.formatOnSubmit(this.nativeFormModel?.values, this.nodeContext);
|
|
428
|
+
}
|
|
429
|
+
return this.nativeFormModel?.values;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
clearValid() {}
|
|
433
|
+
|
|
434
|
+
async validate() {
|
|
435
|
+
this.formFeedbacks = await this.nativeFormModel?.validate();
|
|
436
|
+
this.valid = isEmpty(this.formFeedbacks?.filter((f) => f.level === 'error'));
|
|
437
|
+
this.onValidateEmitter.fire(this);
|
|
438
|
+
return this.valid;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
getValues<T = any>(): T | undefined {
|
|
442
|
+
return this._formControl?._formModel.values;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
getField<
|
|
446
|
+
TValue = FieldValue,
|
|
447
|
+
TField extends IFieldArray<TValue> | IField<TValue> = IField<TValue>
|
|
448
|
+
>(name: FieldName): TField | undefined {
|
|
449
|
+
let finalName = name.includes('/') ? convertGlobPath(name) : name;
|
|
450
|
+
|
|
451
|
+
return this.formControl?.getField<TValue, TField>(finalName) as TField;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
getValueIn<TValue>(name: FieldName): TValue | undefined {
|
|
455
|
+
let finalName = name.includes('/') ? convertGlobPath(name) : name;
|
|
456
|
+
|
|
457
|
+
return this.nativeFormModel?.getValueIn(finalName);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
setValueIn(name: FieldName, value: any) {
|
|
461
|
+
let finalName = name.includes('/') ? convertGlobPath(name) : name;
|
|
462
|
+
|
|
463
|
+
this.nativeFormModel?.setValueIn(finalName, value);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* 监听表单某个路径下的值变化
|
|
468
|
+
* @param name 路径
|
|
469
|
+
* @param callback 回调函数
|
|
470
|
+
*/
|
|
471
|
+
onFormValueChangeIn<TValue = FieldValue, TFormValue = FieldValue>(
|
|
472
|
+
name: FieldName,
|
|
473
|
+
callback: (payload: onFormValueChangeInPayload<TValue, TFormValue>) => void
|
|
474
|
+
): Disposable {
|
|
475
|
+
if (!this._initialized) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`[NodeEngine] FormModel Error: onFormValueChangeIn can not be called before initialized`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return this.formControl!._formModel.onFormValuesChange(
|
|
482
|
+
({ name: changedName, values, prevValues }) => {
|
|
483
|
+
if (changedName === name) {
|
|
484
|
+
callback({
|
|
485
|
+
value: get(values, name),
|
|
486
|
+
prevValue: get(prevValues, name),
|
|
487
|
+
formValues: values,
|
|
488
|
+
prevFormValues: prevValues,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* @deprecated 该方法用于兼容 V1 版本 FormModel接口,如果确定是FormModelV2 请使用 FormModel.getValueIn
|
|
497
|
+
* @param path glob path
|
|
498
|
+
*/
|
|
499
|
+
getFormItemValueByPath(globPath: string) {
|
|
500
|
+
if (!globPath) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (globPath === '/') {
|
|
504
|
+
return this._formControl?._formModel.values;
|
|
505
|
+
}
|
|
506
|
+
const name = convertGlobPath(globPath);
|
|
507
|
+
return this.getValueIn(name!);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async validateWithFeedbacks(): Promise<FormFeedback[]> {
|
|
511
|
+
await this.validate();
|
|
512
|
+
return formFeedbacksToNodeCoreFormFeedbacks(this.formFeedbacks!);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @deprecated 该方法用于兼容 V1 版本 FormModel接口,如果确定是FormModelV2, 请使用FormModel.getValueIn 和 FormModel.setValueIn
|
|
517
|
+
* @param path glob path
|
|
518
|
+
*/
|
|
519
|
+
getFormItemByPath(path: string): FormItem | undefined {
|
|
520
|
+
if (!this.nativeFormModel) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const that = this;
|
|
525
|
+
|
|
526
|
+
if (path === '/') {
|
|
527
|
+
return {
|
|
528
|
+
get value() {
|
|
529
|
+
return that.nativeFormModel!.values;
|
|
530
|
+
},
|
|
531
|
+
set value(v) {
|
|
532
|
+
that.nativeFormModel!.values = v;
|
|
533
|
+
},
|
|
534
|
+
} as FormItem;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const name = convertGlobPath(path);
|
|
538
|
+
const formItemValue = that.getValueIn(name!);
|
|
539
|
+
return {
|
|
540
|
+
get value() {
|
|
541
|
+
return formItemValue;
|
|
542
|
+
},
|
|
543
|
+
set value(v) {
|
|
544
|
+
that.setValueIn(name, v);
|
|
545
|
+
},
|
|
546
|
+
} as FormItem;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
dispose(): void {
|
|
550
|
+
this.onDisposeEmitter.fire();
|
|
551
|
+
|
|
552
|
+
// 执行所有effect return
|
|
553
|
+
this.effectReturnMap.forEach((eventMap) => {
|
|
554
|
+
Object.values(eventMap).forEach((effectReturn) => {
|
|
555
|
+
effectReturn();
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
this.effectMap = DEFAULT.EFFECT_MAP();
|
|
560
|
+
this.effectReturnMap = DEFAULT.EFFECT_RETURN_MAP();
|
|
561
|
+
|
|
562
|
+
this.plugins.forEach((p) => {
|
|
563
|
+
p.dispose();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
this.plugins = [];
|
|
567
|
+
|
|
568
|
+
this.formFeedbacks = DEFAULT.FORM_FEEDBACKS();
|
|
569
|
+
this._valid = DEFAULT.VALID;
|
|
570
|
+
|
|
571
|
+
this._formControl = undefined;
|
|
572
|
+
this._initialized = false;
|
|
573
|
+
this.toDispose.dispose();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
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 { Disposable } from '@flowgram-vue/utils';
|
|
8
|
+
import { type NodeFormContext } from '@flowgram-vue/form-core';
|
|
9
|
+
|
|
10
|
+
import { mergeEffectMap } from './utils';
|
|
11
|
+
import { type FormMeta, type FormPluginCtx, type FormPluginSetupMetaCtx } from './types';
|
|
12
|
+
import { FormModelV2 } from './form-model-v2';
|
|
13
|
+
|
|
14
|
+
export interface FormPluginConfig<Opts = any> {
|
|
15
|
+
/**
|
|
16
|
+
* form plugin name, for debug use
|
|
17
|
+
*/
|
|
18
|
+
name?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* setup formMeta
|
|
22
|
+
* @param ctx
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
onSetupFormMeta?: (ctx: FormPluginSetupMetaCtx, opts: Opts) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* FormModel 初始化时执行
|
|
29
|
+
* @param ctx
|
|
30
|
+
*/
|
|
31
|
+
onInit?: (ctx: FormPluginCtx, opts: Opts) => void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* FormModel 销毁时执行
|
|
35
|
+
*/
|
|
36
|
+
onDispose?: (ctx: FormPluginCtx, opts: Opts) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class FormPlugin<Opts = any> implements Disposable {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
|
|
42
|
+
readonly pluginId: string;
|
|
43
|
+
|
|
44
|
+
readonly config: FormPluginConfig;
|
|
45
|
+
|
|
46
|
+
readonly opts?: Opts;
|
|
47
|
+
|
|
48
|
+
protected _formModel: FormModelV2;
|
|
49
|
+
|
|
50
|
+
constructor(config: FormPluginConfig, opts?: Opts) {
|
|
51
|
+
this.name = config?.name || '';
|
|
52
|
+
this.pluginId = `${this.name}__${nanoid()}`;
|
|
53
|
+
this.config = config;
|
|
54
|
+
|
|
55
|
+
this.opts = opts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get formModel(): FormModelV2 {
|
|
59
|
+
return this._formModel;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get ctx(): { formModel: FormModelV2 } & NodeFormContext {
|
|
63
|
+
return {
|
|
64
|
+
formModel: this.formModel,
|
|
65
|
+
...this.formModel.nodeContext,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setupFormMeta(formMeta: FormMeta, nodeContext: NodeFormContext): FormMeta {
|
|
70
|
+
const nextFormMeta: FormMeta = {
|
|
71
|
+
...formMeta,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
this.config.onSetupFormMeta?.(
|
|
75
|
+
{
|
|
76
|
+
mergeEffect: (effect) => {
|
|
77
|
+
nextFormMeta.effect = mergeEffectMap(nextFormMeta.effect || {}, effect);
|
|
78
|
+
},
|
|
79
|
+
mergeValidate: (validate) => {
|
|
80
|
+
nextFormMeta.validate = {
|
|
81
|
+
...(nextFormMeta.validate || {}),
|
|
82
|
+
...validate,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
addFormatOnInit: (formatOnInit) => {
|
|
86
|
+
if (!nextFormMeta.formatOnInit) {
|
|
87
|
+
nextFormMeta.formatOnInit = formatOnInit;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const legacyFormatOnInit = nextFormMeta.formatOnInit;
|
|
91
|
+
nextFormMeta.formatOnInit = (v, c) => formatOnInit?.(legacyFormatOnInit(v, c), c);
|
|
92
|
+
},
|
|
93
|
+
addFormatOnSubmit: (formatOnSubmit) => {
|
|
94
|
+
if (!nextFormMeta.formatOnSubmit) {
|
|
95
|
+
nextFormMeta.formatOnSubmit = formatOnSubmit;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const legacyFormatOnSubmit = nextFormMeta.formatOnSubmit;
|
|
99
|
+
nextFormMeta.formatOnSubmit = (v, c) => formatOnSubmit?.(legacyFormatOnSubmit(v, c), c);
|
|
100
|
+
},
|
|
101
|
+
...nodeContext,
|
|
102
|
+
},
|
|
103
|
+
this.opts
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return nextFormMeta;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
init(formModel: FormModelV2) {
|
|
110
|
+
this._formModel = formModel;
|
|
111
|
+
this.config?.onInit?.(this.ctx, this.opts);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
dispose() {
|
|
115
|
+
if (this.config?.onDispose) {
|
|
116
|
+
this.config?.onDispose(this.ctx, this.opts);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type FormPluginCreator<Opts> = (opts: Opts) => FormPlugin<Opts>;
|
|
122
|
+
|
|
123
|
+
export function defineFormPluginCreator<Opts>(
|
|
124
|
+
config: FormPluginConfig<Opts>
|
|
125
|
+
): FormPluginCreator<Opts> {
|
|
126
|
+
return function (opts: Opts) {
|
|
127
|
+
return new FormPlugin(config, opts);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { h } from 'vue';
|
|
7
|
+
|
|
8
|
+
import { FormModelV2 } from './form-model-v2';
|
|
9
|
+
import NodeFormRender from './NodeFormRender.vue';
|
|
10
|
+
|
|
11
|
+
export function renderForm(formModel: FormModelV2) {
|
|
12
|
+
return h(NodeFormRender, { formModel });
|
|
13
|
+
}
|