@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,109 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { h, type VNode } from 'vue';
7
+
8
+ import { Disposable, Event } from '@flowgram-vue/utils';
9
+ import { FlowNodeFormData, NodeRender, OnFormValuesChangePayload } from '@flowgram-vue/form-core';
10
+ import { FieldName, FieldValue, FormState } from '@flowgram-vue/form';
11
+ import { FlowNodeEntity } from '@flowgram-vue/document';
12
+
13
+ import { onFormValueChangeInPayload } from './types';
14
+ import { FormModelV2 } from './form-model-v2';
15
+
16
+ export interface NodeFormProps<TValues> {
17
+ /**
18
+ * The initialValues of the form.
19
+ */
20
+ initialValues: TValues;
21
+ /**
22
+ * Form values. Returns a deep copy of the data in the store.
23
+ */
24
+ values: TValues;
25
+ /**
26
+ * Form state
27
+ */
28
+ state: FormState;
29
+ /**
30
+ * Get value in certain path
31
+ * @param name path
32
+ */
33
+ getValueIn<TValue = FieldValue>(name: FieldName): TValue;
34
+
35
+ /**
36
+ * Set value in certain path.
37
+ * It will trigger the re-rendering of the Field Component if a Field is related to this path
38
+ * @param name path
39
+ */
40
+ setValueIn<TValue>(name: FieldName, value: TValue): void;
41
+ /**
42
+ * set form values
43
+ */
44
+ updateFormValues(values: any): void;
45
+ /**
46
+ * Render form
47
+ */
48
+ render: () => VNode;
49
+ /**
50
+ * Form value change event
51
+ */
52
+ onFormValuesChange: Event<OnFormValuesChangePayload>;
53
+ /**
54
+ * Trigger form validate
55
+ */
56
+ validate: () => Promise<boolean>;
57
+ /**
58
+ * Form validate event
59
+ */
60
+ onValidate: Event<FormState>;
61
+ /**
62
+ * Form field value change event
63
+ */
64
+ onFormValueChangeIn<TValue = FieldValue, TFormValue = FieldValue>(
65
+ name: FieldName,
66
+ callback: (payload: onFormValueChangeInPayload<TValue, TFormValue>) => void
67
+ ): Disposable;
68
+ }
69
+
70
+ /**
71
+ * Use `node.form` instead
72
+ * @deprecated
73
+ * @param node
74
+ */
75
+ export function getNodeForm<TValues = FieldValue>(
76
+ node: FlowNodeEntity
77
+ ): NodeFormProps<TValues> | undefined {
78
+ const formModel = node.getData<FlowNodeFormData>(FlowNodeFormData)?.getFormModel<FormModelV2>();
79
+ const nativeFormModel = formModel?.nativeFormModel;
80
+
81
+ if (!formModel || !nativeFormModel) return undefined;
82
+
83
+ const result: NodeFormProps<TValues> = {
84
+ initialValues: nativeFormModel.initialValues,
85
+ get values() {
86
+ return nativeFormModel.values;
87
+ },
88
+
89
+ state: nativeFormModel.state,
90
+ getValueIn: (name: FieldName) => nativeFormModel.getValueIn(name),
91
+ setValueIn: (name: FieldName, value: any) => nativeFormModel.setValueIn(name, value),
92
+ updateFormValues: (values: any) => {
93
+ formModel.updateFormValues(values);
94
+ },
95
+ render: () => h(NodeRender, { node }),
96
+ onFormValuesChange: formModel.onFormValuesChange.bind(formModel),
97
+ onFormValueChangeIn: formModel.onFormValueChangeIn.bind(formModel),
98
+ onValidate: formModel.nativeFormModel.onValidate,
99
+ validate: formModel.validate.bind(formModel),
100
+ };
101
+
102
+ Object.defineProperty(result, '_formModel', {
103
+ enumerable: false,
104
+ get() {
105
+ return formModel;
106
+ },
107
+ });
108
+ return result;
109
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { FlowNodeFormData } from '@flowgram-vue/form-core';
7
+ import { FlowNodeEntity } from '@flowgram-vue/document';
8
+
9
+ import { DataEvent } from './types';
10
+ import { FormModelV2 } from './form-model-v2';
11
+
12
+ export function getFormModel(node: FlowNodeEntity) {
13
+ // @ts-ignore
14
+ return node.getData<FlowNodeFormData>(FlowNodeFormData)?.formModel as FormModelV2;
15
+ }
16
+
17
+ export function isFormV2(node: FlowNodeEntity) {
18
+ return !!node.getNodeRegistry().formMeta?.render;
19
+ }
20
+
21
+ export function createEffectOptions<T>(
22
+ event: DataEvent,
23
+ effect: T
24
+ ): { effect: T; event: DataEvent } {
25
+ return {
26
+ event,
27
+ effect,
28
+ };
29
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { onBeforeUnmount } from 'vue';
7
+
8
+ import { useRefresh } from '@flowgram-vue/utils';
9
+ import { FlowNodeFormData } from '@flowgram-vue/form-core';
10
+ import { Errors, Warnings } from '@flowgram-vue/form';
11
+ import { FormState, useFormErrors, useFormState, useFormWarnings } from '@flowgram-vue/form';
12
+ import { FlowNodeEntity } from '@flowgram-vue/document';
13
+
14
+ import { FormModelV2 } from './form-model-v2';
15
+
16
+ /**
17
+ * Listen to Form's values and refresh the Vue component.
18
+ * By providing related node, you can use this hook outside the Form Component.
19
+ * @param node
20
+ */
21
+ export function useWatchFormValues<T = any>(node: FlowNodeEntity): T | undefined {
22
+ const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
23
+ const refresh = useRefresh();
24
+
25
+ const disposable = formModel.nativeFormModel?.onFormValuesChange(() => {
26
+ refresh();
27
+ });
28
+ onBeforeUnmount(() => disposable?.dispose());
29
+
30
+ return formModel.getValues<T>();
31
+ }
32
+
33
+ /**
34
+ * Listen to Form's value in a certain path and refresh the Vue component.
35
+ * By providing related node, you can use this hook outside the Form Component.
36
+ * @param node
37
+ */
38
+ export function useWatchFormValueIn<T = any>(node: FlowNodeEntity, name: string): T | undefined {
39
+ const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
40
+ const refresh = useRefresh();
41
+
42
+ const disposable = formModel.nativeFormModel?.onFormValuesChange(({ name: changedName }) => {
43
+ if (name === changedName) {
44
+ refresh();
45
+ }
46
+ });
47
+
48
+ onBeforeUnmount(() => disposable?.dispose());
49
+
50
+ return formModel.getValueIn<T>(name);
51
+ }
52
+
53
+ /**
54
+ * Listen to FormModel's initialization and refresh Vue component.
55
+ * By providing related node, you can use this hook outside the Form Component.
56
+ * @param node
57
+ */
58
+ export function useInitializedFormModel(node: FlowNodeEntity) {
59
+ const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
60
+ const refresh = useRefresh();
61
+
62
+ const disposable = formModel.onInitialized(() => {
63
+ refresh();
64
+ });
65
+ onBeforeUnmount(() => disposable.dispose());
66
+
67
+ return formModel;
68
+ }
69
+
70
+ /**
71
+ * Get Form's state, Form State is a proxy, it will refresh the Vue component when the value you accessed changed
72
+ * By providing related node, you can use this hook outside the Form Component.
73
+ * @param node
74
+ */
75
+ export function useWatchFormState(node: FlowNodeEntity): FormState | undefined {
76
+ const formModel = useInitializedFormModel(node);
77
+ return useFormState(formModel.formControl);
78
+ }
79
+
80
+ /**
81
+ * Get Form's errors, Form errors is a proxy, it will refresh the Vue component when the value you accessed changed
82
+ * By providing related node, you can use this hook outside the Form Component.
83
+ * @param node
84
+ */
85
+ export function useWatchFormErrors(node: FlowNodeEntity): Errors | undefined {
86
+ const formModel = useInitializedFormModel(node);
87
+ return useFormErrors(formModel.formControl);
88
+ }
89
+
90
+ /**
91
+ * Get Form's warnings, Form warnings is a proxy, it will refresh the Vue component when the value you accessed changed
92
+ * By providing related node, you can use this hook outside the Form Component.
93
+ * @param node
94
+ */
95
+ export function useWatchFormWarnings(node: FlowNodeEntity): Warnings | undefined {
96
+ const formModel = useInitializedFormModel(node);
97
+ return useFormWarnings(formModel.formControl);
98
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './types';
7
+ export * from './form-model-v2';
8
+ export { isFormV2, createEffectOptions } from './helpers';
9
+ export * from './hooks';
10
+ export * from './form-plugin';
11
+ export { type NodeFormProps, getNodeForm } from './get-node-form';
package/src/types.ts ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import type { VNode } from 'vue';
7
+
8
+ import { FormModel, IFormMeta, NodeContext } from '@flowgram-vue/form-core';
9
+ import { FieldName, FieldValue } from '@flowgram-vue/form';
10
+ import {
11
+ FormRenderProps,
12
+ IForm,
13
+ Validate as FormValidate,
14
+ ValidateTrigger,
15
+ } from '@flowgram-vue/form';
16
+
17
+ import { FormPlugin } from './form-plugin';
18
+ import { FormModelV2 } from './form-model-v2';
19
+
20
+ export interface Node {}
21
+
22
+ export interface Flow {}
23
+
24
+ export type Validate<TFieldValue = any, TFormValues = any> = (props: {
25
+ value: TFieldValue;
26
+ formValues: TFormValues;
27
+ context: NodeContext;
28
+ name: FieldName;
29
+ }) => ReturnType<FormValidate<TFieldValue, TFormValues>>;
30
+
31
+ export enum DataEvent {
32
+ /* When value change */
33
+ onValueChange = 'onValueChange',
34
+ /**
35
+ * When value Init,it triggers when
36
+ * - defaultValue is configured in formMeta, it will trigger when form is initializing.
37
+ * - defaultValue is configured in Field, it will trigger when this Field is initializing if no initial value is set to this field.
38
+ */
39
+ onValueInit = 'onValueInit',
40
+ /**
41
+ * When Value Init or change
42
+ */
43
+ onValueInitOrChange = 'onValueInitOrChange',
44
+ /* It will trigger when ArrayField.append is called. It relies on ArrayField's rendering. If ArrayField is possibly not rendered in your case, please avoid using this event */
45
+ onArrayAppend = 'onArrayAppend',
46
+ /* It will trigger when ArrayField.delete is called. It relies on ArrayField's rendering. If ArrayField is possibly not rendered in your case, please avoid using this event */
47
+ onArrayDelete = 'onArrayDelete',
48
+ }
49
+
50
+ export type EffectReturn = () => void;
51
+
52
+ export interface EffectFuncProps<TFieldValue = any, TFormValues = any> {
53
+ name: FieldName;
54
+ value: TFieldValue;
55
+ prevValue?: TFieldValue;
56
+ formValues: TFormValues;
57
+ form: IForm;
58
+ context: NodeContext;
59
+ }
60
+
61
+ export type Effect<TFieldValue = any, TFormValues = any> = (
62
+ props: EffectFuncProps<TFieldValue, TFormValues>
63
+ ) => void | EffectReturn;
64
+
65
+ export type ArrayAppendEffect<TFieldValue = any, TFormValues = any> = (props: {
66
+ index: number;
67
+ value: TFieldValue;
68
+ arrayValues: Array<TFieldValue>;
69
+ formValues: TFormValues;
70
+ form: IForm;
71
+ context: NodeContext;
72
+ }) => void | EffectReturn;
73
+
74
+ export type ArrayDeleteEffect<TFieldValue = any, TFormValues = any> = (props: {
75
+ index: number;
76
+ arrayValue: Array<TFieldValue>;
77
+ formValues: TFormValues;
78
+ form: IForm;
79
+ context: NodeContext;
80
+ }) => void | EffectReturn;
81
+
82
+ export type EffectOptions =
83
+ | { effect: Effect; event: DataEvent }
84
+ | { effect: ArrayAppendEffect; event: DataEvent }
85
+ | { effect: ArrayDeleteEffect; event: DataEvent };
86
+
87
+ export interface FormMeta<TValues = any> {
88
+ /**
89
+ * The render method of the node form content. <Form /> is already integrated, so you don't need to wrap your components with <Form />
90
+ * @param props
91
+ */
92
+ render: (props: FormRenderProps<any>) => VNode;
93
+ /**
94
+ * When to trigger the validation.
95
+ */
96
+ validateTrigger?: ValidateTrigger;
97
+ /**
98
+ * Form data's validation rules. It's a key value map, where the key is a pattern of data's path (or field name), the value is a validate function.
99
+ */
100
+ validate?:
101
+ | Record<FieldName, Validate>
102
+ | ((values: TValues, ctx: NodeContext) => Record<FieldName, Validate>);
103
+ /**
104
+ * Form data's effects. It's a key value map, where the key is a pattern of data's path (or field name), the value is an array of effect configuration.
105
+ */
106
+ effect?: Record<FieldName, EffectOptions[]>;
107
+ /**
108
+ * Form data's complete default value. it will not be sent to formatOnInit, but used directly as form's value when needed.
109
+ */
110
+ defaultValues?: TValues | ((context: NodeContext) => TValues);
111
+ /**
112
+ * This function is to format the value when initiate the form, the returned value will be used as the initial value of the form.
113
+ * @param value value input to node as initialValue.
114
+ * @param context
115
+ */
116
+ formatOnInit?: (value: any, context: NodeContext) => any;
117
+ /**
118
+ * This function is to format the value when FormModel.toJSON is called, the returned value will be used as the final value to be saved .
119
+ * @param value value sent by form before format.
120
+ * @param context
121
+ */
122
+ formatOnSubmit?: (value: any, context: NodeContext) => any;
123
+ /**
124
+ * Form's plugins
125
+ */
126
+ plugins?: FormPlugin[];
127
+ }
128
+
129
+ export function isFormModelV2(fm: FormModel | FormModelV2): fm is FormModelV2 {
130
+ return 'onFormValuesChange' in fm;
131
+ }
132
+
133
+ export function isFormMetaV2(formMeta: IFormMeta | FormMeta) {
134
+ return 'render' in formMeta;
135
+ }
136
+
137
+ export type FormPluginCtx = {
138
+ formModel: FormModelV2;
139
+ } & NodeContext;
140
+
141
+ export type FormPluginSetupMetaCtx = {
142
+ mergeEffect: (effect: Record<string, EffectOptions[]>) => void;
143
+ mergeValidate: (validate: Record<FieldName, Validate>) => void;
144
+ addFormatOnInit: (formatOnInit: FormMeta['formatOnInit']) => void;
145
+ addFormatOnSubmit: (formatOnSubmit: FormMeta['formatOnSubmit']) => void;
146
+ } & NodeContext;
147
+
148
+ export interface onFormValueChangeInPayload<TValue = FieldValue, TFormValues = FieldValue> {
149
+ value: TValue;
150
+ prevValue: TValue;
151
+ formValues: TFormValues;
152
+ prevFormValues: TFormValues;
153
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { find, mergeWith } from 'lodash-es';
7
+ import { FormFeedback, FormPathService } from '@flowgram-vue/form-core';
8
+ import type { FieldError, FieldWarning, FormValidateReturn } from '@flowgram-vue/form';
9
+ import { type FieldModel, FieldName } from '@flowgram-vue/form';
10
+
11
+ import { DataEvent, EffectOptions, EffectReturn } from './types';
12
+
13
+ export function findMatchedInMap<T = any>(
14
+ field: FieldModel<any>,
15
+ validateMap: Record<FieldName, T> | undefined
16
+ ): T | undefined {
17
+ if (!validateMap) {
18
+ return;
19
+ }
20
+ if (validateMap[field.name]) {
21
+ return validateMap[field.name];
22
+ }
23
+
24
+ const found = find(Object.keys(validateMap), (key) => {
25
+ if (key.startsWith('regex:')) {
26
+ const regex = RegExp(key.split(':')[1]);
27
+ return regex.test(field.name);
28
+ }
29
+ return false;
30
+ });
31
+
32
+ if (found) {
33
+ return validateMap[found];
34
+ }
35
+ }
36
+
37
+ export function formFeedbacksToNodeCoreFormFeedbacks(
38
+ formFeedbacks: FormValidateReturn
39
+ ): FormFeedback[] {
40
+ return formFeedbacks.map(
41
+ (f: FieldError | FieldWarning) =>
42
+ ({
43
+ feedbackStatus: f.level,
44
+ feedbackText: f.message,
45
+ path: f.name,
46
+ } as FormFeedback)
47
+ );
48
+ }
49
+
50
+ export function convertGlobPath(path: string) {
51
+ if (path.startsWith('/')) {
52
+ const parts = FormPathService.normalize(path).slice(1).split('/');
53
+ return parts.join('.');
54
+ }
55
+ return path;
56
+ }
57
+
58
+ export function mergeEffectMap(
59
+ origin: Record<string, EffectOptions[]>,
60
+ source: Record<string, EffectOptions[]>
61
+ ) {
62
+ return mergeWith(origin, source, function (objValue: EffectOptions[], srcValue: EffectOptions[]) {
63
+ return (objValue || []).concat(srcValue);
64
+ });
65
+ }
66
+
67
+ export function mergeEffectReturn(origin?: EffectReturn, source?: EffectReturn): EffectReturn {
68
+ return () => {
69
+ origin?.();
70
+ source?.();
71
+ };
72
+ }
73
+
74
+ export function runAndDeleteEffectReturn(
75
+ effectReturnMap: Map<DataEvent, Record<string, EffectReturn>>,
76
+ name: string,
77
+ events: DataEvent[]
78
+ ) {
79
+ events.forEach((event) => {
80
+ const eventMap = effectReturnMap.get(event);
81
+ if (eventMap?.[name]) {
82
+ eventMap[name]();
83
+ delete eventMap[name];
84
+ }
85
+ });
86
+ }