Outputting Variables

We mainly divide output variables into three categories:

  1. Output Node Variables: Usually as the output of that node for subsequent nodes to use.
  2. Output Node Private Variables: Output variables are limited to the inside of the node (including child nodes) and cannot be accessed by external nodes.
  3. Output Global Variables: Runs through the entire process, and any node can read it. It is suitable for storing some public states or configurations.

Outputting Node Variables

Outputting a node variable means that this variable is bound to the life cycle of the current node. When the node is created, the variable is born; when the node is deleted, the variable also disappears.

We usually have three ways to output node variables:

Method 1: Sync via Form Side Effects

Form Side Effects are usually configured in the node's form-meta.ts file and are the most common way to define node output variables.

provideJsonSchemaOutputs Material

If the structure of the output variable required by the node matches the JSON Schema structure, you can use the provideJsonSchemaOutputs side effect (Effect) material.

It is very simple to use, just add two lines of configuration in the effect of formMeta:

form-meta.ts
import {
  syncVariableTitle,
  provideJsonSchemaOutputs,
} from '@flowgram.ai/form-materials';

export const formMeta = {
  effect: {
    title: syncVariableTitle, // Variable title is automatically synchronized
    outputs: provideJsonSchemaOutputs,
  },
};

Via createEffectFromVariableProvider

provideJsonSchemaOutputs only adapts to JsonSchema. If you want to define your own set of Schema, then you need to customize the side effects of the form.

NOTE

Flowgram provides createEffectFromVariableProvider, you only need to define a parse function to customize your own variable synchronization side effects:

  • parse will be called when the form value is initialized and updated
  • The input of parse is the value of the current field's form
  • The output of parse is the variable AST information

In the following example, we create output variables for the two fields of the form path.to.value and path.to.value2:

form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
  type ASTNodeJSON
} from '@flowgram.ai/fixed-layout-editor';

export function createTypeFromValue(value: string): ASTNodeJSON | undefined {
  switch (value) {
    case 'string':
      return ASTFactory.createString();
    case 'number':
      return ASTFactory.createNumber();
    case 'boolean':
      return ASTFactory.createBoolean();
    case 'integer':
      return ASTFactory.createInteger();

    default:
      return;
  }
}

export const formMeta =  {
  effect: {
    // Create first variable
    // = node.scope.setVar('path.to.value', ASTFactory.createVariableDeclaration(parse(v)))
    'path.to.value': createEffectFromVariableProvider({
      // parse form value to variable
      parse(v: string) {
        return {
          meta: {
            title: `Your Output Variable Title`,
          },
          key: `uid_${node.id}`,
          type: createTypeFromValue(v)
        }
      }
    }),
    // Create second variable
    // = node.scope.setVar('path.to.value2', ASTFactory.createVariableDeclaration(parse(v)))
    'path.to.value2': createEffectFromVariableProvider({
      // parse form value to variable
      parse(v: string) {
        return {
          meta: {
            title: `Your Output Variable Title 2`,
          },
          key: `uid_${node.id}_2`,
          type: createTypeFromValue(v)
        }
      }
    }),
  },
  render: () => (
    // ...
  )
}

Syncing multiple form fields to one variable

If multiple fields are synchronized to one variable, you need to use the namespace field of createEffectFromVariableProvider to synchronize the variable data of multiple fields to the same namespace.

form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
} from '@flowgram.ai/fixed-layout-editor';

/**
 * Get information of multiple fields from the form
 */
const variableSyncEffect = createEffectFromVariableProvider({
  // Must be added to ensure that the side effects of different fields are synchronized to the same namespace
  namespace: 'your_namespace',

  // Parse the form value into a variable
  parse(_, { form, node }) {
    return {
      meta: {
        title: `Your Output Variable Title`,
      },
      key: `uid_${node.id}`,
      type: createTypeFromValue({
        value1: form.getValueIn('path.to.value'),
        value2: form.getValueIn('path.to.value2'),
      })
    }
  }
})

export const formMeta = {
  effect: {
    'path.to.value': variableSyncEffect,
    'path.to.value2': variableSyncEffect,
  },
  render: () => (
   // ...
  )
}

Using the node.scope API in Side Effects

If createEffectFromVariableProvider does not meet your needs, you can also directly use the node.scope API in form side effects for more flexible and variable operations.

NOTE

node.scope will return a node's variable scope (Scope) object, which has several core methods:

  • setVar(variable): Set a variable.
  • setVar(namespace, variable): Set a variable under the specified namespace.
  • getVar(): Get all variables.
  • getVar(namespace): Get the variables under the specified namespace.
  • clearVar(): Clear all variables.
  • clearVar(namespace): Clear the variables under the specified namespace.
form-meta.tsx
import { Effect } from '@flowgram.ai/editor';

export const formMeta = {
  effect: {
    'path.to.value': [{
      event: DataEvent.onValueInitOrChange,
      effect: ((params) => {
        const { context, value } = params;

        context.node.scope.setVar(
          ASTFactory.createVariableDeclaration({
            meta: {
              title: `Your Output Variable Title`,
            },
            key: `uid_${node.id}`,
            type: createTypeFromValue(value),
          })
        )

        console.log("View generated variables", context.node.scope.getVar())

      }) as Effect,
    }],
    'path.to.value2': [{
      event: DataEvent.onValueInitOrChange,
      effect: ((params) => {
        const { context, value } = params;

        context.node.scope.setVar(
          'namespace_2',
          ASTFactory.createVariableDeclaration({
            meta: {
              title: `Your Output Variable Title 2`,
            },
            key: `uid_${node.id}_2`,
            type: createTypeFromValue(value),
          })
        )

        console.log("View generated variables", context.node.scope.getVar('namespace_2'))

      }) as Effect,
    }],
  },
  render: () => (
    // ...
  )
}

Method 2: Sync Variables via Plugins

In addition to static configuration in the form, we can also freely and dynamically operate the variables of the node in the plugin (Plugin) through node.scope.

Update the Scope of the specified node

The following example demonstrates how to get the Scope of the start node in the onInit life cycle of the plugin and perform a series of operations on its variables.

sync-variable-plugin.tsx
import {
  FlowDocument,
  definePluginCreator,
  PluginCreator,
} from '@flowgram.ai/fixed-layout-editor';

export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({
    onInit(ctx, options) {
      const startNode = ctx.get(FlowDocument).getNode('start_0');
      const startScope =  startNode.scope!

      // Set Variable For Start Scope
      startScope.setVar(
        ASTFactory.createVariableDeclaration({
          meta: {
            title: `Your Output Variable Title`,
          },
          key: `uid`,
          type: ASTFactory.createString(),
        })
      )
    }
  })

Sync variables in onNodeCreate

The following example demonstrates how to get the Scope of a newly created node through onNodeCreate and synchronize variables by listening to node.form.onFormValuesChange.

sync-variable-plugin.tsx
import {
  FlowDocument,
  definePluginCreator,
  PluginCreator,
} from '@flowgram.ai/fixed-layout-editor';

export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({
    onInit(ctx, options) {
      ctx.get(FlowDocument).onNodeCreate(({ node }) => {
        const syncVariable = (title: string) => {
          node.scope?.setVar(
            ASTFactory.createVariableDeclaration({
              key: `uid_${node.id}`,
              meta: {
                title,
                icon: iconVariable,
              },
              type: ASTFactory.createString(),
            })
          );
        };

        if (node.form) {
          // sync variable on init
          syncVariable(node.form.getValueIn('title'));

          // listen to form values change
          node.form?.onFormValuesChange(({ values, name }) => {
            // title field changed
            if (name.match(/^title/)) {
              syncVariable(values[name]);
            }
          });
        }
      });
    }
  })
WARNING

Directly synchronizing variables in the UI (Method 3) is a highly discouraged practice.

It breaks the principle of separation of data and rendering, leading to tight coupling between data and rendering:

  • Unable to sync variables without UI: Variables cannot be updated independently without the UI, leading to inconsistencies between data and rendering.
  • Increased code complexity: Directly manipulating variables in the UI increases the complexity of the UI logic, making the code harder to maintain.
  • Performance issues: Variable synchronization operations may trigger unnecessary re-rendering of UI components.
form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
} from '@flowgram.ai/fixed-layout-editor';

/**
 * Get information of multiple fields from the form
 */
const FormRender = () => {
  /**
   * Get the current scope for subsequent variable setting
   */
  const scope = useCurrentScope()

  return <>
    <UserCustomForm
      onValuesChange={(values) => {
        scope.setVar(
          ASTFactory.createVariableDeclaration({
            meta: {
              title: values.title,
            },
            key: `uid`,
            type: ASTFactory.createString(),
          })
        )
      }}
    />
  </>
}

export const formMeta = {
  render: () => <FormRender />
}

Outputting Node Private Variables

Private variables are variables that can only be accessed within the current node and its child nodes.

Private variables can be set and obtained through the private scope node.privateScope. Its scope chain relationship is shown in the following figure:

Only two of the methods are listed below, and other methods can be deduced from the Output Node Variables method.

Method 1: Via createEffectFromVariableProvider

createEffectFromVariableProvider provides the parameter scope to specify the scope of the variable.

  • When scope is set to private, the scope of the variable is the private scope of the current node node.privateScope
  • When scope is set to public, the scope of the variable is the scope of the current node node.scope
form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
} from '@flowgram.ai/fixed-layout-editor';

export const formMeta =  {
  effect: {
    // Create variable in privateScope
    // = node.privateScope.setVar('path.to.value', ASTFactory.createVariableDeclaration(parse(v)))
    'path.to.value': createEffectFromVariableProvider({
      scope: 'private',
      // parse form value to variable
      parse(v: string) {
        return {
          meta: {
            title: `Your Private Variable Title`,
          },
          key: `uid_${node.id}_locals`,
          type: createTypeFromValue(v)
        }
      }
    }),
  },
  render: () => (
    // ...
  )
}

Method 2: Via node.privateScope

The API of node.privateScope is designed to be almost identical to the node scope (node.scope), providing methods such as setVar, getVar, clearVar, and also supporting namespaces. For details, please refer to node.scope.

form-meta.tsx
import { Effect } from '@flowgram.ai/editor';

export const formMeta = {
  effect: {
    'path.to.value': [{
      event: DataEvent.onValueInitOrChange,
      effect: ((params) => {
        const { context, value } = params;

        context.node.privateScope.setVar(
          ASTFactory.createVariableDeclaration({
            meta: {
              title: `Your Private Variable Title`,
            },
            key: `uid_${node.id}`,
            type: createTypeFromValue(value),
          })
        )

        console.log("View generated variables", context.node.privateScope.getVar())

      }) as Effect,
    }],
  },
  render: () => (
    // ...
  )
}

Outputting Global Variables

Global variables are like the "shared memory" of the entire process, which can be accessed and modified by any node and any plugin. It is very suitable for storing some states that run through, such as user information, environment configuration, and so on.

Similar to node variables, we also have two main ways to obtain the scope of global variables (GlobalScope).

Method 1: Obtain in Plugin

In the context of the plugin (ctx), we can directly "inject" the instance of GlobalScope:

global-variable-plugin.tsx
import {
  GlobalScope,
  definePluginCreator,
  PluginCreator
} from '@flowgram.ai/fixed-layout-editor';


export const createGlobalVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({
    onInit(ctx, options) {
      const globalScope = ctx.get(GlobalScope)

      globalScope.setVar(
         ASTFactory.createVariableDeclaration({
          meta: {
            title: `Your Output Variable Title`,
          },
          key: `your_variable_global_unique_key`,
          type: ASTFactory.createString(),
        })
      )
    }
  })

Method 2: Obtain in UI

If you want to interact with global variables in the React component of the canvas, you can use the useService Hook to get the instance of GlobalScope:

global-variable-component.tsx
import {
  GlobalScope,
  useService,
} from '@flowgram.ai/fixed-layout-editor';

function GlobalVariableComponent() {

  const globalScope = useService(GlobalScope)

  // ...

  const handleChange = (v: string) => {
    globalScope.setVar(
      ASTFactory.createVariableDeclaration({
        meta: {
          title: `Your Output Variable Title`,
        },
        key: `uid_${v}`,
        type: ASTFactory.createString(),
      })
    )
  }

  return <Input onChange={handleChange}/>
}

API of Global Scope

The API of GlobalScope is designed to be almost identical to the node scope (node.scope), providing methods such as setVar, getVar, clearVar, and also supporting namespaces. For details, please refer to node.scope.

Here is a comprehensive example of operating global variables in a plugin:

sync-variable-plugin.tsx
import {
  GlobalScope,
} from '@flowgram.ai/fixed-layout-editor';

// ...

onInit(ctx, options) {
  const globalScope = ctx.get(GlobalScope);

  // 1. Create, Update, Read, Delete Variable in GlobalScope
  globalScope.setVar(
    ASTFactory.createVariableDeclaration({
      meta: {
        title: `Your Output Variable Title`,
      },
      key: `your_variable_global_unique_key`,
      type: ASTFactory.createString(),
    })
  )

  console.log(globalScope.getVar())

  globalScope.clearVar()


  // 2.  Create, Update, Read, Delete Variable in GlobalScope's namespace: 'namespace_1'
    globalScope.setVar(
      'namespace_1',
      ASTFactory.createVariableDeclaration({
        meta: {
          title: `Your Output Variable Title 2`,
        },
        key: `uid_2`,
        type: ASTFactory.createString(),
      })
  )

  console.log(globalScope.getVar('namespace_1'))

  globalScope.clearVar('namespace_1')

  // ...
}

See also: Class: GlobalScope