| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- # Creating a Free Layout Canvas
- This example can be installed via `npx @flowgram.ai/create-app@latest free-layout-simple`. For complete code and demo, see:
- <div className="rs-tip">
- <a className="rs-link" href="/en/examples/free-layout/free-layout-simple.html">
- Free Layout Basic Usage
- </a>
- </div>
- File structure:
- ```
- - hooks
- - use-editor-props.ts # Canvas configuration
- - components
- - base-node.tsx # Node rendering
- - tools.tsx # Canvas toolbar
- - initial-data.ts # Initialization data
- - node-registries.ts # Node configuration
- - app.tsx # Canvas entry
- ```
- ### 1. Canvas Entry
- - `FreeLayoutEditorProvider`: Canvas configurator that generates react-context internally for child components to consume
- - `EditorRenderer`: The final rendered canvas that can be wrapped under other components for customizing canvas position
- ```tsx pure title="app.tsx"
- import {
- FreeLayoutEditorProvider,
- EditorRenderer,
- } from '@flowgram.ai/free-layout-editor';
- import '@flowgram.ai/free-layout-editor/index.css'; // Load styles
- import { useEditorProps } from './hooks/use-editor-props' // Detailed canvas props configuration
- import { Tools } from './components/tools' // Canvas tools
- function App() {
- const editorProps = useEditorProps()
- return (
- <FreeLayoutEditorProvider {...editorProps}>
- <EditorRenderer className="demo-editor" />
- <Tools />
- </FreeLayoutEditorProvider>
- );
- }
- ```
- ### 2. Configure Canvas
- Canvas configuration is declarative, providing data, rendering, events, and plugin-related configurations
- ```tsx pure title="hooks/use-editor-props.tsx"
- import { useMemo } from 'react';
- import { type FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
- import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
- import { initialData } from './initial-data' // Initialization data
- import { nodeRegistries } from './node-registries' // Node declaration configuration
- import { BaseNode } from './components/base-node' // Node rendering
- export function useEditorProps(
- ): FreeLayoutProps {
- return useMemo<FreeLayoutProps>(
- () => ({
- /**
- * Initialization data
- */
- initialData,
- /**
- * Canvas node definitions
- */
- nodeRegistries,
- /**
- * Materials
- */
- materials: {
- renderDefaultNode: BaseNode, // Node rendering component
- },
- /**
- * Node engine for rendering node forms
- */
- nodeEngine: {
- enable: true,
- },
- /**
- * Canvas history record for controlling redo/undo
- */
- history: {
- enable: true,
- enableChangeNode: true, // For monitoring node form data changes
- },
- /**
- * Canvas initialization callback
- */
- onInit: ctx => {
- // For dynamic data loading, you can use the following method asynchronously
- // ctx.docuemnt.fromJSON(initialData)
- },
- /**
- * Callback when canvas first renders completely
- */
- onAllLayersRendered: (ctx) => {},
- /**
- * Canvas destruction callback
- */
- onDispose: () => { },
- plugins: () => [
- /**
- * Minimap plugin
- */
- createMinimapPlugin({}),
- ],
- }),
- [],
- );
- }
- ```
- ### 3. Configure Data
- Canvas document data uses a tree structure that supports nesting
- :::note Document Data Basic Structure:
- - nodes `array` Node list, supports nesting
- - edges `array` Edge list
- :::
- :::note Node Data Basic Structure:
- - id: `string` Unique node identifier, must be unique
- - meta: `object` Node UI configuration information, such as free layout `position` information
- - type: `string | number` Node type, corresponds to `type` in `nodeRegistries`
- - data: `object` Node form data, customizable by business
- - blocks: `array` Node branches, using `block` is closer to `Gramming`
- - edges: `array` Edges for the sub-canvas
- :::
- :::note Edge Data Basic Structure:
- - sourceNodeID: `string` Start node id
- - targetNodeID: `string` Target node id
- - sourcePortID?: `string | number` Start port id, defaults to start node's default port if omitted
- - targetPortID?: `string | number` Target port id, defaults to target node's default port if omitted
- :::
- ```tsx pure title="initial-data.ts"
- import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
- export const initialData: WorkflowJSON = {
- nodes: [
- {
- id: 'start_0',
- type: 'start',
- meta: {
- position: { x: 0, y: 0 },
- },
- data: {
- title: 'Start',
- content: 'Start content'
- },
- },
- {
- id: 'node_0',
- type: 'custom',
- meta: {
- position: { x: 400, y: 0 },
- },
- data: {
- title: 'Custom',
- content: 'Custom node content'
- },
- },
- {
- id: 'end_0',
- type: 'end',
- meta: {
- position: { x: 800, y: 0 },
- },
- data: {
- title: 'End',
- content: 'End content'
- },
- },
- ],
- edges: [
- {
- sourceNodeID: 'start_0',
- targetNodeID: 'node_0',
- },
- {
- sourceNodeID: 'node_0',
- targetNodeID: 'end_0',
- },
- ],
- };
- ```
- ### 4. Declare Nodes
- Node declarations can be used to determine node types and rendering methods
- ```tsx pure title="node-registries.tsx"
- import { WorkflowNodeRegistry, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
- /**
- * You can customize your own node registry
- */
- export const nodeRegistries: WorkflowNodeRegistry[] = [
- {
- type: 'start',
- meta: {
- isStart: true, // Mark as start node
- deleteDisable: true, // Start node cannot be deleted
- copyDisable: true, // Start node cannot be copied
- defaultPorts: [{ type: 'output' }], // Define node input and output ports, start node only has output port
- // useDynamicPort: true, // For dynamic ports, will look for DOM with data-port-id and data-port-type attributes as ports
- },
- /**
- * Configure node form validation and rendering
- * Note: validate uses data and rendering separation to ensure node validation even without rendering
- */
- formMeta: {
- validateTrigger: ValidateTrigger.onChange,
- validate: {
- title: ({ value }) => (value ? undefined : 'Title is required'),
- },
- /**
- * Render form
- */
- render: () => (
- <>
- <Field name="title">
- {({ field }) => <div className="demo-free-node-title">{field.value}</div>}
- </Field>
- <Field name="content">
- {({ field }) => <input onChange={field.onChange} value={field.value}/>}
- </Field>
- </>
- )
- },
- },
- {
- type: 'end',
- meta: {
- deleteDisable: true,
- copyDisable: true,
- defaultPorts: [{ type: 'input' }],
- },
- formMeta: {
- // ...
- }
- },
- {
- type: 'custom',
- meta: {
- },
- formMeta: {
- // ...
- },
- defaultPorts: [{ type: 'output' }, { type: 'input' }], // Normal nodes have two ports
- },
- ];
- ```
- ### 5. Render Nodes
- Node rendering is used to add styles, events, and form rendering positions
- ```tsx pure title="components/base-node.tsx"
- import { useNodeRender, WorkflowNodeRenderer } from '@flowgram.ai/free-layout-editor';
- export const BaseNode = () => {
- /**
- * Provides node rendering related methods
- */
- const { form } = useNodeRender()
- /**
- * WorkflowNodeRenderer adds node drag events and port rendering, for deep customization, see the component source code:
- * https://github.com/bytedance/flowgram.ai/blob/main/packages/client/free-layout-editor/src/components/workflow-node-renderer.tsx
- */
- return (
- <WorkflowNodeRenderer className="demo-free-node" node={props.node}>
- {
- // Form rendering generated through formMeta
- form?.render()
- }
- </WorkflowNodeRenderer>
- )
- };
- ```
- ### 6. Add Tools
- Tools are mainly used to control canvas zoom and other operations. Tools are collected in `usePlaygroundTools`, while `useClientContext` is used to get canvas context, which contains core canvas modules like `history`
- ```tsx pure title="components/tools.tsx"
- import { useEffect, useState } from 'react'
- import { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';
- export function Tools() {
- const { history } = useClientContext();
- const tools = usePlaygroundTools();
- const [canUndo, setCanUndo] = useState(false);
- const [canRedo, setCanRedo] = useState(false);
- useEffect(() => {
- const disposable = history.undoRedoService.onChange(() => {
- setCanUndo(history.canUndo());
- setCanRedo(history.canRedo());
- });
- return () => disposable.dispose();
- }, [history]);
- return <div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}>
- <button onClick={() => tools.zoomin()}>ZoomIn</button>
- <button onClick={() => tools.zoomout()}>ZoomOut</button>
- <button onClick={() => tools.fitView()}>Fitview</button>
- <button onClick={() => tools.autoLayout()}>AutoLayout</button>
- <button onClick={() => history.undo()} disabled={!canUndo}>Undo</button>
- <button onClick={() => history.redo()} disabled={!canRedo}>Redo</button>
- <span>{Math.floor(tools.zoom * 100)}%</span>
- </div>
- }
- ```
- ### 7. Result
- import { FreeLayoutSimple } from '../../../../components';
- <div style={{ position: 'relative', width: '100%', height: '600px'}}>
- <FreeLayoutSimple />
- </div>
|