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