create-free-layout-simple.mdx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. # Creating a Free Layout Canvas
  2. This example can be installed via `npx @flowgram.ai/create-app@latest free-layout-simple`. For complete code and demo, see:
  3. <div className="rs-tip">
  4. <a className="rs-link" href="/en/examples/free-layout/free-layout-simple.html">
  5. Free Layout Basic Usage
  6. </a>
  7. </div>
  8. File structure:
  9. ```
  10. - hooks
  11. - use-editor-props.ts # Canvas configuration
  12. - components
  13. - base-node.tsx # Node rendering
  14. - tools.tsx # Canvas toolbar
  15. - initial-data.ts # Initialization data
  16. - node-registries.ts # Node configuration
  17. - app.tsx # Canvas entry
  18. ```
  19. ### 1. Canvas Entry
  20. - `FreeLayoutEditorProvider`: Canvas configurator that generates react-context internally for child components to consume
  21. - `EditorRenderer`: The final rendered canvas that can be wrapped under other components for customizing canvas position
  22. ```tsx pure title="app.tsx"
  23. import {
  24. FreeLayoutEditorProvider,
  25. EditorRenderer,
  26. } from '@flowgram.ai/free-layout-editor';
  27. import '@flowgram.ai/free-layout-editor/index.css'; // Load styles
  28. import { useEditorProps } from './hooks/use-editor-props' // Detailed canvas props configuration
  29. import { Tools } from './components/tools' // Canvas tools
  30. function App() {
  31. const editorProps = useEditorProps()
  32. return (
  33. <FreeLayoutEditorProvider {...editorProps}>
  34. <EditorRenderer className="demo-editor" />
  35. <Tools />
  36. </FreeLayoutEditorProvider>
  37. );
  38. }
  39. ```
  40. ### 2. Configure Canvas
  41. Canvas configuration is declarative, providing data, rendering, events, and plugin-related configurations
  42. ```tsx pure title="hooks/use-editor-props.tsx"
  43. import { useMemo } from 'react';
  44. import { type FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
  45. import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
  46. import { initialData } from './initial-data' // Initialization data
  47. import { nodeRegistries } from './node-registries' // Node declaration configuration
  48. import { BaseNode } from './components/base-node' // Node rendering
  49. export function useEditorProps(
  50. ): FreeLayoutProps {
  51. return useMemo<FreeLayoutProps>(
  52. () => ({
  53. /**
  54. * Initialization data
  55. */
  56. initialData,
  57. /**
  58. * Canvas node definitions
  59. */
  60. nodeRegistries,
  61. /**
  62. * Materials
  63. */
  64. materials: {
  65. renderDefaultNode: BaseNode, // Node rendering component
  66. },
  67. /**
  68. * Node engine for rendering node forms
  69. */
  70. nodeEngine: {
  71. enable: true,
  72. },
  73. /**
  74. * Canvas history record for controlling redo/undo
  75. */
  76. history: {
  77. enable: true,
  78. enableChangeNode: true, // For monitoring node form data changes
  79. },
  80. /**
  81. * Canvas initialization callback
  82. */
  83. onInit: ctx => {
  84. // For dynamic data loading, you can use the following method asynchronously
  85. // ctx.docuemnt.fromJSON(initialData)
  86. },
  87. /**
  88. * Callback when canvas first renders completely
  89. */
  90. onAllLayersRendered: (ctx) => {},
  91. /**
  92. * Canvas destruction callback
  93. */
  94. onDispose: () => { },
  95. plugins: () => [
  96. /**
  97. * Minimap plugin
  98. */
  99. createMinimapPlugin({}),
  100. ],
  101. }),
  102. [],
  103. );
  104. }
  105. ```
  106. ### 3. Configure Data
  107. Canvas document data uses a tree structure that supports nesting
  108. :::note Document Data Basic Structure:
  109. - nodes `array` Node list, supports nesting
  110. - edges `array` Edge list
  111. :::
  112. :::note Node Data Basic Structure:
  113. - id: `string` Unique node identifier, must be unique
  114. - meta: `object` Node UI configuration information, such as free layout `position` information
  115. - type: `string | number` Node type, corresponds to `type` in `nodeRegistries`
  116. - data: `object` Node form data, customizable by business
  117. - blocks: `array` Node branches, using `block` is closer to `Gramming`
  118. - edges: `array` Edges for the sub-canvas
  119. :::
  120. :::note Edge Data Basic Structure:
  121. - sourceNodeID: `string` Start node id
  122. - targetNodeID: `string` Target node id
  123. - sourcePortID?: `string | number` Start port id, defaults to start node's default port if omitted
  124. - targetPortID?: `string | number` Target port id, defaults to target node's default port if omitted
  125. :::
  126. ```tsx pure title="initial-data.ts"
  127. import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
  128. export const initialData: WorkflowJSON = {
  129. nodes: [
  130. {
  131. id: 'start_0',
  132. type: 'start',
  133. meta: {
  134. position: { x: 0, y: 0 },
  135. },
  136. data: {
  137. title: 'Start',
  138. content: 'Start content'
  139. },
  140. },
  141. {
  142. id: 'node_0',
  143. type: 'custom',
  144. meta: {
  145. position: { x: 400, y: 0 },
  146. },
  147. data: {
  148. title: 'Custom',
  149. content: 'Custom node content'
  150. },
  151. },
  152. {
  153. id: 'end_0',
  154. type: 'end',
  155. meta: {
  156. position: { x: 800, y: 0 },
  157. },
  158. data: {
  159. title: 'End',
  160. content: 'End content'
  161. },
  162. },
  163. ],
  164. edges: [
  165. {
  166. sourceNodeID: 'start_0',
  167. targetNodeID: 'node_0',
  168. },
  169. {
  170. sourceNodeID: 'node_0',
  171. targetNodeID: 'end_0',
  172. },
  173. ],
  174. };
  175. ```
  176. ### 4. Declare Nodes
  177. Node declarations can be used to determine node types and rendering methods
  178. ```tsx pure title="node-registries.tsx"
  179. import { WorkflowNodeRegistry, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
  180. /**
  181. * You can customize your own node registry
  182. */
  183. export const nodeRegistries: WorkflowNodeRegistry[] = [
  184. {
  185. type: 'start',
  186. meta: {
  187. isStart: true, // Mark as start node
  188. deleteDisable: true, // Start node cannot be deleted
  189. copyDisable: true, // Start node cannot be copied
  190. defaultPorts: [{ type: 'output' }], // Define node input and output ports, start node only has output port
  191. // useDynamicPort: true, // For dynamic ports, will look for DOM with data-port-id and data-port-type attributes as ports
  192. },
  193. /**
  194. * Configure node form validation and rendering
  195. * Note: validate uses data and rendering separation to ensure node validation even without rendering
  196. */
  197. formMeta: {
  198. validateTrigger: ValidateTrigger.onChange,
  199. validate: {
  200. title: ({ value }) => (value ? undefined : 'Title is required'),
  201. },
  202. /**
  203. * Render form
  204. */
  205. render: () => (
  206. <>
  207. <Field name="title">
  208. {({ field }) => <div className="demo-free-node-title">{field.value}</div>}
  209. </Field>
  210. <Field name="content">
  211. {({ field }) => <input onChange={field.onChange} value={field.value}/>}
  212. </Field>
  213. </>
  214. )
  215. },
  216. },
  217. {
  218. type: 'end',
  219. meta: {
  220. deleteDisable: true,
  221. copyDisable: true,
  222. defaultPorts: [{ type: 'input' }],
  223. },
  224. formMeta: {
  225. // ...
  226. }
  227. },
  228. {
  229. type: 'custom',
  230. meta: {
  231. },
  232. formMeta: {
  233. // ...
  234. },
  235. defaultPorts: [{ type: 'output' }, { type: 'input' }], // Normal nodes have two ports
  236. },
  237. ];
  238. ```
  239. ### 5. Render Nodes
  240. Node rendering is used to add styles, events, and form rendering positions
  241. ```tsx pure title="components/base-node.tsx"
  242. import { useNodeRender, WorkflowNodeRenderer } from '@flowgram.ai/free-layout-editor';
  243. export const BaseNode = () => {
  244. /**
  245. * Provides node rendering related methods
  246. */
  247. const { form } = useNodeRender()
  248. /**
  249. * WorkflowNodeRenderer adds node drag events and port rendering, for deep customization, see the component source code:
  250. * https://github.com/bytedance/flowgram.ai/blob/main/packages/client/free-layout-editor/src/components/workflow-node-renderer.tsx
  251. */
  252. return (
  253. <WorkflowNodeRenderer className="demo-free-node" node={props.node}>
  254. {
  255. // Form rendering generated through formMeta
  256. form?.render()
  257. }
  258. </WorkflowNodeRenderer>
  259. )
  260. };
  261. ```
  262. ### 6. Add Tools
  263. 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`
  264. ```tsx pure title="components/tools.tsx"
  265. import { useEffect, useState } from 'react'
  266. import { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';
  267. export function Tools() {
  268. const { history } = useClientContext();
  269. const tools = usePlaygroundTools();
  270. const [canUndo, setCanUndo] = useState(false);
  271. const [canRedo, setCanRedo] = useState(false);
  272. useEffect(() => {
  273. const disposable = history.undoRedoService.onChange(() => {
  274. setCanUndo(history.canUndo());
  275. setCanRedo(history.canRedo());
  276. });
  277. return () => disposable.dispose();
  278. }, [history]);
  279. return <div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}>
  280. <button onClick={() => tools.zoomin()}>ZoomIn</button>
  281. <button onClick={() => tools.zoomout()}>ZoomOut</button>
  282. <button onClick={() => tools.fitView()}>Fitview</button>
  283. <button onClick={() => tools.autoLayout()}>AutoLayout</button>
  284. <button onClick={() => history.undo()} disabled={!canUndo}>Undo</button>
  285. <button onClick={() => history.redo()} disabled={!canRedo}>Redo</button>
  286. <span>{Math.floor(tools.zoom * 100)}%</span>
  287. </div>
  288. }
  289. ```
  290. ### 7. Result
  291. import { FreeLayoutSimple } from '../../../../components';
  292. <div style={{ position: 'relative', width: '100%', height: '600px'}}>
  293. <FreeLayoutSimple />
  294. </div>