Merge branch 'main' into refactor/promised-panzoom

This commit is contained in:
moklick
2024-07-04 22:44:45 +02:00
492 changed files with 30971 additions and 17811 deletions
+4 -4
View File
@@ -1,11 +1,11 @@
{ {
"$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json", "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "wbkd/react-flow" }], "changelog": ["@changesets/changelog-github", { "repo": "xyflow/xyflow" }],
"commit": false, "commit": false,
"fixed": [], "fixed": [],
"linked": [], "linked": [],
"access": "public", "access": "restricted",
"baseBranch": "main", "baseBranch": "main",
"updateInternalDependencies": "patch", "updateInternalDependencies": "patch",
"ignore": [] "ignore": ["react-examples", "svelte-examples", "astro-examples", "playwright"]
} }
+38 -42
View File
@@ -4,47 +4,57 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thank you for reporting an issue :pray:. Hello! :sparkles: You found the place to submit a bug or issue that you discovered in the xyflow library, thank you for helping us out!
**Please only file issues for v11. Will will no longer fix issues for v10.** Having trouble making something work in xyflow, but its not a bug with the library itself? Youll get the fastest response by asking in [github discussions](https://github.com/xyflow/xyflow/discussions) or our [discord server](https://discord.gg/RVmnytFmGW).
This issue tracker is for reporting bugs found in react-flow (https://github.com/wbkd/react-flow) **Search our [discussions](https://github.com/xyflow/xyflow/discussions?discussions_q=) and [issues](https://github.com/xyflow/xyflow/issues?q=)** to see if your problem has already been reported and you can add more context, or see if someone else has already solved your issue. And of course, check our [docs](https://reactflow.dev/learn) if you havent already.
If you have a question about how to achieve something and are struggling, please post a question
inside of react-flow's Discussion's tab: https://github.com/wbkd/react-flow/discussions
If it's an issue about the docs please go to the react-flow-docs (https://github.com/wbkd/react-flow-docs)
Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already: **Issue with the xyflow Docs?** Submit an issue in our [web repo](https://github.com/xyflow/web/issues) instead.
- react-flow's Open Issue's tab: https://github.com/wbkd/react-flow/issues?q=is%3Aissue+sort%3Aupdated-desc+position
- react-flow's Closed Issues tab: https://github.com/wbkd/react-flow/issues?q=is%3Aissue+sort%3Aupdated-desc+position+is%3Aclosed
- react-flow's Discussion's tab: https://github.com/wbkd/react-flow/discussions
The more information you fill in, the better the community can help you. With that out of the way, lets get started :bug:
- type: textarea - type: textarea
id: description id: platform
attributes: attributes:
label: Describe the Bug label: What platform were you using when you found the bug?
description: Provide a clear and concise description of the challenge you are running into. description: We only address issues for the most recent xyflow major release or upcoming releases.
value: |
- React Flow / Svelte Flow version:
- Browser and version:
- OS and version:
validations: validations:
required: true required: true
- type: input - type: input
id: link id: link
attributes: attributes:
label: Your Example Website or App label: Live code example
description: | description: |
Which website or app were you using when the bug happened? This is the most important part of the bug report!
Note:
- Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the `react-flow-renderer` npm package. React Flow codesandbox starters
- To create a shareable code example you can use Codesandbox (https://codesandbox.io/s/react-flow-starter-ou8or) or Stackblitz (https://stackblitz.com/). - js: https://new.reactflow.dev/js
- Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. - ts: https://new.reactflow.dev/ts
placeholder: |
e.g. https://stackblitz.com/edit/...... OR Github Repo Svelte Flow stackblitz starters
- js: https://new.svelteflow.dev/js
- ts: https://new.svelteflow.dev/ts
Avoid dependencies in your example for easier debugging.
placeholder: https://stackblitz.com/edit/... or Github Repo
validations: validations:
required: false required: false
- type: textarea
id: description
attributes:
label: Describe the Bug
description: What exactly is happening when you encounter the bug?
validations:
required: true
- type: textarea - type: textarea
id: steps id: steps
attributes: attributes:
label: Steps to Reproduce the Bug or Issue label: Steps to reproduce the bug or issue
description: Describe the steps we have to take to reproduce the behavior. description: How can we see the reported behavior in the code example you provided? The more detail, the better!
placeholder: | placeholder: |
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
@@ -56,9 +66,8 @@ body:
id: expected id: expected
attributes: attributes:
label: Expected behavior label: Expected behavior
description: Provide a clear and concise description of what you expected to happen. description: What would you expect to happen if there was no bug or issue?
placeholder: | placeholder: As a user, I expected ___ behavior, but instead I am seeing ___
As a user, I expected ___ behavior but i am seeing ___
validations: validations:
required: true required: true
- type: textarea - type: textarea
@@ -67,22 +76,9 @@ body:
label: Screenshots or Videos label: Screenshots or Videos
description: | description: |
If applicable, add screenshots or a video to help explain your problem. If applicable, add screenshots or a video to help explain your problem.
For more information on the supported file image/file types and the file size limits, please refer Supported image/file types and file size limits- https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files placeholder: Drag your video or image files inside of this editor
placeholder: |
You can drag your video or image files inside of this editor ↓
- type: textarea
id: platform
attributes:
label: Platform
value: |
- OS: [e.g. macOS, Windows, Linux]
- Browser: [e.g. Chrome, Safari, Firefox]
- Version: [e.g. 91.1]
validations:
required: true
- type: textarea - type: textarea
id: additional id: additional
attributes: attributes:
label: Additional context label: Additional context
description: Add any other context about the problem here.
+11
View File
@@ -0,0 +1,11 @@
name: 'CI checks'
runs:
using: 'composite'
steps:
- name: Build
run: pnpm build
shell: bash
- name: Typecheck
run: pnpm typecheck
shell: bash
+14
View File
@@ -0,0 +1,14 @@
name: 'CI setup'
runs:
using: 'composite'
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Install dependencies
run: pnpm install
shell: bash
+25
View File
@@ -0,0 +1,25 @@
name: Playwright Tests
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.45.0-jammy
steps:
- name: Checkout repo
uses: actions/checkout@v4
- uses: ./.github/actions/ci-setup
- uses: ./.github/actions/ci-checks
- name: Run your tests
run: CI=true pnpm test:react
env:
HOME: /root
+30
View File
@@ -0,0 +1,30 @@
name: Release
on:
workflow_dispatch:
# push:
# branches:
# - main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- uses: ./.github/actions/ci-setup
- uses: ./.github/actions/ci-checks
- name: Create release PR or publish to npm
id: changesets
uses: xyflow/changeset-action@v1
with:
publish: pnpm release
title: Release packages
commit: 'chore(packages): bump'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
-1
View File
@@ -1,3 +1,2 @@
registry="https://registry.npmjs.com/"
legacy-peer-deps=true legacy-peer-deps=true
strict-peer-dependencies=false strict-peer-dependencies=false
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019-2023 webkid GmbH Copyright (c) 2019-2024 webkid GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+14 -5
View File
@@ -17,22 +17,22 @@ Powerful open source libraries for building node-based UIs with React or Svelte.
## The xyflow mono repo ## The xyflow mono repo
The xyflow repository is the home of four packages: The xyflow repository is the home of four packages:
* React Flow v11 `reactflow` [v11 branch](https://github.com/xyflow/xyflow/tree/v11) * React Flow 11 `reactflow` [v11 branch](https://github.com/xyflow/xyflow/tree/v11)
* React Flow v12 (not published yet) [packages/react](./packages/react) * React Flow 12 (beta) `@xyflow/react` [packages/react](./packages/react)
* Svelte Flow `@xyflow/svelte` [packages/svelte](./packages/svelte) * Svelte Flow `@xyflow/svelte` [packages/svelte](./packages/svelte)
* Shared helper library `@xyflow/system` [packages/system](./packages/system) * Shared helper library `@xyflow/system` [packages/system](./packages/system)
**We just moved repositories** from the @wbkd org to this one. React Flow v11 will remain on the [v11 branch](https://github.com/xyflow/xyflow/tree/v11). When we have a stable v12, the package name of React Flow will change from `reactflow` to `@xyflow/react`. **We just moved repositories** from the @wbkd org to this one. React Flow v11 will remain on the [v11 branch](https://github.com/xyflow/xyflow/tree/v11). When we have a stable v12, the package name of React Flow will change from `reactflow` to `@xyflow/react`.
## Commercial Usage ## Commercial usage
**Are you using React Flow or Svelte Flow for a personal project?** Great! No sponsorship needed, you can support us by reporting any bugs you find, sending us screenshots of your projects, and starring us on Github 🌟 **Are you using React Flow or Svelte Flow for a personal project?** Great! No sponsorship needed, you can support us by reporting any bugs you find, sending us screenshots of your projects, and starring us on Github 🌟
**Are you using React Flow or Svelte Flow at your organization and making money from it?** Awesome! We rely on your support to keep our libraries developed and maintained under an MIT License, just how we like it. For React Flow you can do that on the [React Flow Pro website](https://reactflow.dev/pro) and for both of our libraries you can do it through [Github Sponsors](https://github.com/sponsors/xyflow). **Are you using React Flow or Svelte Flow at your organization and making money from it?** Awesome! We rely on your support to keep our libraries developed and maintained under an MIT License, just how we like it. For React Flow you can do that on the [React Flow Pro website](https://reactflow.dev/pro) and for both of our libraries you can do it through [Github Sponsors](https://github.com/sponsors/xyflow).
## Getting Started ## Getting started
The best way to get started is to check out the [React Flow](https://reactflow.dev/learn) or [Svelte Flow](https://svelteflow.dev/learn) learn section. However if you want to get a sneak peek of how to install the and use the libraries you can see it here: The best way to get started is to check out the [React Flow](https://reactflow.dev/learn) or [Svelte Flow](https://svelteflow.dev/learn) learn section. However if you want to get a sneak peek of how to install and use the libraries you can see it here:
<details> <details>
<summary><strong>React Flow</strong> basic usage</summary> <summary><strong>React Flow</strong> basic usage</summary>
@@ -151,6 +151,15 @@ npm install @xyflow/svelte
``` ```
</details> </details>
## Releases
For releasing packages we are using [changesets](https://github.com/changesets/changesets) in combination with the [changeset Github action](https://github.com/changesets/action). The rough idea is:
1. create PRs for new features, updates and fixes (with a changeset if relevant for changelog)
2. merge into main
3. changset creates a PR that bumps all packages based on the changesets
4. merge changeset PR if you want to release to Github and npm
## The xyflow team ## The xyflow team
React Flow and Svelte Flow are maintained by the team behind [xyflow](https://xyflow.com). If you need help or want to talk to us about a collaboration, reach out through our [contact form](https://xyflow.com/contact) or by joining our [Discord Server](https://discord.gg/Bqt6xrs). React Flow and Svelte Flow are maintained by the team behind [xyflow](https://xyflow.com). If you need help or want to talk to us about a collaboration, reach out through our [contact form](https://xyflow.com/contact) or by joining our [Discord Server](https://discord.gg/Bqt6xrs).
+13
View File
@@ -0,0 +1,13 @@
# Examples for Testing & Development
These examples are used for feature development and E2E testing.
## Overview
SvelteKit App with Svelte Flow examples at [`examples/svelte`](./svelte)
Vite App with React Flow examples at [`examples/react`](./react)
Astro App used for SSR testing at [`examples/astro-xyflow`](./astro-xyflow)
## E2E
For furhter documentation of E2E tests have a look at [`/tests/playwright`](/tests/playwright).
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "astro", "name": "astro-examples",
"type": "module", "type": "module",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.1",
@@ -7,7 +7,7 @@ const sourceHandleStyleB: CSSProperties = {
left: 'auto', left: 'auto',
}; };
const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => { const CustomNode: FC<NodeProps> = ({ data, positionAbsoluteX, positionAbsoluteY }) => {
return ( return (
<> <>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
@@ -18,7 +18,7 @@ const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
<div> <div>
Position:{' '} Position:{' '}
<strong> <strong>
{xPos.toFixed(2)},{yPos.toFixed(2)} {positionAbsoluteX.toFixed(2)},{positionAbsoluteY.toFixed(2)}
</strong> </strong>
</div> </div>
</div> </div>
@@ -27,7 +27,7 @@ const initialNodes: Node[] = [
type: 'input', type: 'input',
data: { label: 'Node 1' }, data: { label: 'Node 1' },
position: { x: 250, y: 5 }, position: { x: 250, y: 5 },
size: nodeSize, ...nodeSize,
handles: [ handles: [
{ {
type: 'source', type: 'source',
@@ -41,23 +41,19 @@ const initialNodes: Node[] = [
id: '2', id: '2',
data: { label: 'Node 2' }, data: { label: 'Node 2' },
position: { x: 100, y: 100 }, position: { x: 100, y: 100 },
size: nodeSize, ...nodeSize,
handles: [ handles: [
{ {
type: 'source', type: 'source',
position: Position.Bottom, position: Position.Bottom,
x: nodeSize.width * 0.5, x: nodeSize.width * 0.5,
y: nodeSize.height, y: nodeSize.height,
width: 1,
height: 1,
}, },
{ {
type: 'target', type: 'target',
position: Position.Top, position: Position.Top,
x: nodeSize.width * 0.5, x: nodeSize.width * 0.5,
y: 0, y: 0,
width: 1,
height: 1,
}, },
], ],
}, },
@@ -65,23 +61,19 @@ const initialNodes: Node[] = [
id: '3', id: '3',
data: { label: 'Node 3' }, data: { label: 'Node 3' },
position: { x: 400, y: 100 }, position: { x: 400, y: 100 },
size: nodeSize, ...nodeSize,
handles: [ handles: [
{ {
type: 'source', type: 'source',
position: Position.Bottom, position: Position.Bottom,
x: nodeSize.width * 0.5, x: nodeSize.width * 0.5,
y: nodeSize.height, y: nodeSize.height,
width: 1,
height: 1,
}, },
{ {
type: 'target', type: 'target',
position: Position.Top, position: Position.Top,
x: nodeSize.width * 0.5, x: nodeSize.width * 0.5,
y: 0, y: 0,
width: 1,
height: 1,
}, },
], ],
}, },
@@ -89,6 +81,8 @@ const initialNodes: Node[] = [
id: '4', id: '4',
data: { label: 'Node 4' }, data: { label: 'Node 4' },
position: { x: 400, y: 200 }, position: { x: 400, y: 200 },
width: 200,
height: 50,
type: 'custom', type: 'custom',
}, },
]; ];
@@ -0,0 +1,21 @@
import { memo, useState } from 'react';
import { Handle, Position } from '@xyflow/react';
function CustomNode() {
const [text, setText] = useState('this is a pretty long text');
return (
<>
<Handle type="target" position={Position.Top} />
<div style={{ background: '#eee', padding: 10 }}>
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<div>text: {text}</div>
</div>
</div>
<Handle type="source" position={Position.Bottom} />
</>
);
}
export default memo(CustomNode);
@@ -0,0 +1,69 @@
import { useCallback } from 'react';
import {
ReactFlow,
addEdge,
useEdgesState,
useNodesState,
Background,
Controls,
type Connection,
type Edge,
type Node,
} from '@xyflow/react';
import CustomNode from './CustomNode';
import '@xyflow/react/dist/style.css';
const initialNodes: Node[] = [
{
id: '1',
data: {},
position: { x: 0, y: 0 },
initialWidth: 200,
initialHeight: 50,
type: 'custom',
},
{
id: '2',
data: {},
position: { x: 0, y: 200 },
width: 200,
initialHeight: 50,
type: 'custom',
},
];
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const nodeTypes = {
custom: CustomNode,
};
function Flow() {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<div style={{ width: 700, height: 400 }}>
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
width={700}
height={400}
debug
>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
export default Flow;
@@ -11,10 +11,8 @@
data: { label: 'Node 0' }, data: { label: 'Node 0' },
sourcePosition: Position.Right, sourcePosition: Position.Right,
targetPosition: Position.Left, targetPosition: Position.Left,
size: { width: 100,
width: 100, height: 40,
height: 40,
},
handles: [ handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right }, { type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left }, { type: 'target', x: 0, y: 20, position: Position.Left },
@@ -26,10 +24,8 @@
data: { label: 'A' }, data: { label: 'A' },
sourcePosition: Position.Right, sourcePosition: Position.Right,
targetPosition: Position.Left, targetPosition: Position.Left,
size: { width: 100,
width: 100, height: 40,
height: 40,
},
handles: [ handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right }, { type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left }, { type: 'target', x: 0, y: 20, position: Position.Left },
@@ -41,10 +37,8 @@
data: { label: 'B' }, data: { label: 'B' },
sourcePosition: Position.Right, sourcePosition: Position.Right,
targetPosition: Position.Left, targetPosition: Position.Left,
size: { width: 100,
width: 100, height: 40,
height: 40,
},
handles: [ handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right }, { type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left }, { type: 'target', x: 0, y: 20, position: Position.Left },
@@ -56,10 +50,8 @@
data: { label: 'C' }, data: { label: 'C' },
sourcePosition: Position.Right, sourcePosition: Position.Right,
targetPosition: Position.Left, targetPosition: Position.Left,
size: { width: 100,
width: 100, height: 40,
height: 40,
},
handles: [ handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right }, { type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left }, { type: 'target', x: 0, y: 20, position: Position.Left },
@@ -0,0 +1,25 @@
<script lang="ts">
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
type $$Props = NodeProps;
let text = 'some default text';
</script>
<Handle type="target" position={Position.Left} />
<div class="custom">
<div>
text: {text}
</div>
<input type="text" bind:value={text} />
</div>
<Handle type="source" position={Position.Right} />
<style>
.custom {
background-color: white;
padding: 10px;
border: 1px solid #777;
border-radius: 20px;
}
</style>
@@ -0,0 +1,39 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Controls, Background, BackgroundVariant, type Node, type Edge } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import CustomNode from './CustomNode.svelte';
const nodes = writable<Node[]>([
{
id: '1',
position: { x: 0, y: 0 },
data: {},
initialWidth: 100,
initialHeight: 40,
type: 'input-node',
},
{
id: '2',
position: { x: 0, y: 150 },
data: {},
initialWidth: 100,
initialHeight: 40,
type: 'input-node',
},
]);
const edges = writable<Edge[]>([{ id: '1-2', source: '1', target: '2' }]);
const nodeTypes = {
'input-node': CustomNode,
};
</script>
<div style="height: 400px; width: 700px;">
<SvelteFlow {nodes} {edges} {nodeTypes} fitView width={700} height={400}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
</SvelteFlow>
</div>
+20 -4
View File
@@ -1,6 +1,8 @@
--- ---
import ReactFlowApp from '../components/ReactFlowExample' import ReactFlowApp from '../components/ReactFlowExample';
import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte' import ReactFlowInitialApp from '../components/ReactFlowInitialExample';
import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte';
import SvelteFlowInitialApp from '../components/SvelteFlowInitialExample/index.svelte';
--- ---
<html lang="en"> <html lang="en">
@@ -18,10 +20,24 @@ import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte'
</style> </style>
</head> </head>
<body> <body>
<h2>React Flow</h2>
<p>no client hydration</p>
<ReactFlowApp />
<p>client hydration on load (client:load)</p>
<ReactFlowApp client:load />
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
<ReactFlowInitialApp client:load />
<h2>Svelte Flow</h2> <h2>Svelte Flow</h2>
<SvelteFlowApp /> <SvelteFlowApp />
<h2>React Flow</h2> <p>client hydration on load (client:load)</p>
<ReactFlowApp /> <SvelteFlowApp client:load />
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
<SvelteFlowInitialApp client:load />
</body> </body>
</html> </html>
+11 -1
View File
@@ -1,6 +1,6 @@
# React Flow examples # React Flow examples
This Vite app is used internally to test the library. This Vite app is used internally to develop and test the library.
## Start local dev server ## Start local dev server
@@ -8,5 +8,15 @@ This Vite app is used internally to test the library.
pnpm dev pnpm dev
``` ```
## Adding new example
Development of the library is done against `src/examples`. Feel free to add new implementations for features that you develop.
1. Create a new folder & flow at `src/examples/`
2. Register the new route in `src/App/routes.ts`
## Adding new E2E implementation
E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under [`http://localhost:3000/tests/generic/$foldername/$filename`](http://localhost:5173/tests/generic/nodes/general). For further documentation visit [`xyflow/tests/playwright`](/tests/playwright).
+1 -1
View File
@@ -2,7 +2,7 @@ import { defineConfig } from 'cypress';
export default defineConfig({ export default defineConfig({
e2e: { e2e: {
baseUrl: 'http://localhost:3000', baseUrl: 'http://localhost:3000/examples',
viewportWidth: 1280, viewportWidth: 1280,
viewportHeight: 720, viewportHeight: 720,
video: false, video: false,
@@ -13,7 +13,6 @@ describe('useEdges.cy.tsx', () => {
</ReactFlow> </ReactFlow>
); );
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges); cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
}); });
@@ -15,8 +15,9 @@ const initialNodes: Node[] = nodes.map((n) => ({
const expectedNodes: Node[] = initialNodes.map((n) => ({ const expectedNodes: Node[] = initialNodes.map((n) => ({
...n, ...n,
positionAbsolute: n.position, measured: {
...nodeDimensions, ...nodeDimensions,
},
})); }));
describe('useNodes.cy.tsx', () => { describe('useNodes.cy.tsx', () => {
@@ -29,7 +30,6 @@ describe('useNodes.cy.tsx', () => {
</ReactFlow> </ReactFlow>
); );
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes); cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
}); });
@@ -6,7 +6,7 @@ import * as simpleflow from '../../fixtures/simpleflow';
describe('<ReactFlow />: Basic Props', () => { describe('<ReactFlow />: Basic Props', () => {
describe('uses defaultNodes and defaultEdges', () => { describe('uses defaultNodes and defaultEdges', () => {
beforeEach(() => { beforeEach(() => {
cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} />); cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} nodeDragThreshold={0} />);
}); });
it('mounts nodes and edges', () => { it('mounts nodes and edges', () => {
@@ -0,0 +1,138 @@
import { useCallback, useState } from 'react';
import { OnNodesChange, Panel, ReactFlow, ReactFlowProps, applyNodeChanges, useReactFlow, Node } from '@xyflow/react';
const nodes = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
style: { width: 100, height: 40 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 200, y: 200 },
style: { width: 80, height: 50 },
},
];
describe('<ReactFlow />: onNodesChange', () => {
beforeEach(() => {
const onNodesChange = cy.spy().as('onNodesChange');
cy.mount(<Comp nodes={nodes} onNodesChange={onNodesChange} />).wait(500);
});
it('receive initial dimension change', () => {
const expectedChanges = nodes.map((n) => ({
type: 'dimensions',
id: n.id,
dimensions: n.style,
}));
cy.get('@onNodesChange').should('have.callCount', 1);
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
it('receive replace and dimension change', () => {
const expectedDimensionChanges = [
{
type: 'dimensions',
id: '1',
dimensions: { width: 200, height: 100 },
},
];
const expectedReplaceChanges = [
{
type: 'replace',
id: '1',
item: {
...nodes[0],
measured: { width: 200, height: 100 },
style: { width: 200, height: 100 },
},
},
];
cy.get('[data-id=update-btn]').click();
cy.get('@onNodesChange').should('have.callCount', 3);
cy.get('@onNodesChange').then((s) => {
// dimension change (already checked above)
// const firstCall = s.getCall(0);
// replace change
// @ts-ignore
const secondChangeCallArgs = s.getCall(1).args[0];
// dimension change
// @ts-ignore
const thirdChangeCallArgs = s.getCall(2).args[0];
expect(secondChangeCallArgs).to.be.deep.eq(expectedReplaceChanges);
expect(thirdChangeCallArgs).to.be.deep.eq(expectedDimensionChanges);
});
});
it('receive select and unselect change', () => {
const expectedSelectChanges = [{ id: '1', type: 'select', selected: true }];
const expectedUnselectChanges = [{ id: '1', type: 'select', selected: false }];
cy.get('.react-flow__node').first().click();
cy.get('@onNodesChange').should('have.calledWith', expectedSelectChanges);
cy.get('.react-flow__pane').first().click({ force: true });
cy.get('@onNodesChange').should('have.calledWith', expectedUnselectChanges);
});
it('receive position change', () => {
const endPosition = { x: 200, y: 25 };
const expectedChanges = [
{ id: '1', type: 'position', dragging: false, position: endPosition, positionAbsolute: endPosition },
];
cy.drag('.react-flow__node:first', endPosition).then(() => {
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
});
it('receive remove change', () => {
const expectedChanges = [{ id: '1', type: 'remove' }];
cy.get('.react-flow__node').first().click();
cy.realPress('Backspace');
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
});
// test specific helpers
function UpdateButton() {
const { updateNode } = useReactFlow();
const updateNodeDimensions = () => {
updateNode('1', { style: { width: 200, height: 100 } });
};
return (
<Panel position="top-right">
<button data-id="update-btn" onClick={updateNodeDimensions} id="btn">
update
</button>
</Panel>
);
}
type CompProps = ReactFlowProps & { onNodesChange: (changes: any) => void };
function Comp(props: CompProps) {
const [nodes, setNodes] = useState(props.nodes || []);
const onNodesChange: OnNodesChange<Node> = useCallback((changes) => {
props.onNodesChange(changes);
setNodes((nds) => applyNodeChanges(changes, nds));
}, []);
return (
<ReactFlow nodes={nodes} onNodesChange={onNodesChange} nodeDragThreshold={0}>
<UpdateButton />
</ReactFlow>
);
}
@@ -0,0 +1,178 @@
import {
applyNodeChanges,
applyEdgeChanges,
type Node,
type Edge,
NodeChange,
EdgeChange,
XYPosition,
} from '@xyflow/react';
const nodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const edges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2-3', source: '2', target: '3' },
];
describe('applyChanges Testing', () => {
it('adds a node/edge', () => {
const currentNodes: Node[] = [];
const nodeChanges: NodeChange[] = [{ type: 'add', item: nodes[0] }];
const nextNodes = applyNodeChanges(nodeChanges, currentNodes);
expect(nextNodes.length).to.be.equal(nodeChanges.length);
const currentEdges: Edge[] = [];
const edgeChanges: EdgeChange[] = [{ type: 'add', item: edges[0] }];
const nextEdges = applyEdgeChanges(edgeChanges, currentEdges);
expect(nextEdges.length).to.be.equal(1);
});
it('adds multiple nodes/edges', () => {
const currentNodes: Node[] = [];
const nodeChanges: NodeChange[] = [
{ type: 'add', item: nodes[0] },
{ type: 'add', item: nodes[1] },
];
const nextNodes = applyNodeChanges(nodeChanges, currentNodes);
expect(nextNodes.length).to.be.equal(nodeChanges.length);
const currentEdges: Edge[] = [];
const edgeChanges: EdgeChange[] = [
{ type: 'add', item: edges[0] },
{ type: 'add', item: edges[1] },
];
const nextEdges = applyEdgeChanges(edgeChanges, currentEdges);
expect(nextEdges.length).to.be.equal(edgeChanges.length);
});
it('removes a node/edge', () => {
const nodesLength = nodes.length;
const nodeChanges: NodeChange[] = [{ type: 'remove', id: '1' }];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes.length).to.be.equal(nodes.length - nodeChanges.length);
expect(nodes.length).to.be.equal(nodesLength);
const edgesLength = edges.length;
const edgeChanges: EdgeChange[] = [{ type: 'remove', id: 'e1-2' }];
const nextEdges = applyEdgeChanges(edgeChanges, edges);
expect(nextEdges.length).to.be.equal(edges.length - 1);
expect(edges.length).to.be.equal(edgesLength);
});
it('removes multiple node/edge', () => {
const nodesLength = nodes.length;
const nodeChanges: NodeChange[] = [
{ type: 'remove', id: '1' },
{ type: 'remove', id: '2' },
];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes.length).to.be.equal(nodes.length - nodeChanges.length);
expect(nodes.length).to.be.equal(nodesLength);
const edgesLength = edges.length;
const edgeChanges: EdgeChange[] = [
{ type: 'remove', id: 'e1-2' },
{ type: 'remove', id: 'e1-3' },
];
const nextEdges = applyEdgeChanges(edgeChanges, edges);
expect(nextEdges.length).to.be.equal(edges.length - edgeChanges.length);
expect(edges.length).to.be.equal(edgesLength);
});
it('selects a node/edge', () => {
const nodeChanges: NodeChange[] = [{ type: 'select', id: '1', selected: true }];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes[0].selected).to.be.true;
const edgeChanges: EdgeChange[] = [{ type: 'select', id: 'e1-2', selected: true }];
const nextEdges = applyEdgeChanges(edgeChanges, edges);
expect(nextEdges[0].selected).to.be.true;
});
it('deselects a node/edge', () => {
const nodeChanges: NodeChange[] = [{ type: 'select', id: '1', selected: false }];
const nextNodes = applyNodeChanges(
nodeChanges,
nodes.map((n) => ({ ...n, selected: true }))
);
expect(nextNodes[0].selected).to.be.false;
expect(nextNodes[1].selected).to.be.true;
const edgeChanges: EdgeChange[] = [{ type: 'select', id: 'e1-2', selected: false }];
const nextEdges = applyEdgeChanges(
edgeChanges,
edges.map((e) => ({ ...e, selected: true }))
);
expect(nextEdges[0].selected).to.be.false;
expect(nextEdges[1].selected).to.be.true;
});
it('updates node position', () => {
const newPosition: XYPosition = { x: 100, y: 100 };
const nodeChanges: NodeChange[] = [{ type: 'position', id: '1', position: newPosition }];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes[0].position).to.be.deep.equal(newPosition);
});
it('updates node dimensions', () => {
const newWidth = 200;
const newHeight = 200;
const nodeChanges: NodeChange[] = [
{ type: 'dimensions', id: '1', dimensions: { width: newWidth, height: newHeight } },
];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nodes[0].measured).to.be.undefined;
expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
expect(nextNodes[0].width).to.be.undefined;
expect(nextNodes[0].height).to.be.undefined;
});
it('updates node position and dimensions', () => {
const newPosition: XYPosition = { x: 100, y: 100 };
const newWidth = 200;
const newHeight = 200;
const nodeChanges: NodeChange[] = [
{ type: 'position', id: '1', position: newPosition },
{ type: 'dimensions', id: '1', dimensions: { width: newWidth, height: newHeight } },
];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes[0].position).to.be.deep.equal(newPosition);
expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
});
it('replaces nodes/edges', () => {
const nodesLength = nodes.length;
const nodeChanges: NodeChange[] = [{ type: 'replace', id: nodes[0].id, item: nodes[1] }];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nodes.length).length.to.be.equal(nodesLength);
expect(nextNodes.length).to.be.equal(nodesLength);
expect(nextNodes[0]).to.be.deep.equal(nodes[1]);
const edgesLength = edges.length;
const edgeChange: EdgeChange[] = [{ type: 'replace', id: edges[0].id, item: edges[1] }];
const nextEdges = applyEdgeChanges(edgeChange, edges);
expect(edges.length).length.to.be.equal(edgesLength);
expect(nextEdges.length).to.be.equal(edgesLength);
expect(nextEdges[0]).to.be.deep.equal(edges[1]);
});
});
export {};
+18 -24
View File
@@ -49,21 +49,16 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
it('selects one node with a selection', () => { it('selects one node with a selection', () => {
cy.get('body') cy.get('body')
.type('{shift}', { release: false }) .type('{Shift}', { release: false })
.wait(50)
.get('.react-flow__pane') .get('.react-flow__pane')
.trigger('mousedown', 1, 10, { button: 0, force: true }) .trigger('mousedown', 1, 10, { button: 0, force: true })
.trigger('mousemove', 1000, 450, { button: 0 }) .trigger('mousemove', 1000, 200, { button: 0 })
.wait(50) .trigger('mouseup', 1000, 200, { button: 0 });
.trigger('mouseup', 1000, 450, { button: 0, force: true });
cy.wait(200); cy.get('.react-flow__node').eq(0).should('have.class', 'selected');
cy.get('.react-flow__node').eq(1).should('have.class', 'selected');
cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected'); cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected');
cy.get('.react-flow__nodesselection-rect'); cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true, force: true }); cy.get('body').type('{shift}', { release: true, force: true });
}); });
@@ -97,19 +92,19 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
it('drags a node', () => { it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform'); const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el: any) => { cy.drag('.react-flow__node:first', { x: 10, y: 10 }).then(($el: any) => {
const styleAfterDrag = $el.css('transform'); const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag); expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
}); });
}); });
// @TODO: why does this fail since react18? it('removes a node', () => {
// it('removes a node', () => { cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
// cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected'); cy.get('html').realPress('Backspace');
// cy.get('html').type('{backspace}').wait(100);
// cy.get('.react-flow__node').should('have.length', 3); cy.get('.react-flow__node').should('have.length', 3);
// cy.get('.react-flow__edge').should('have.length', 1); cy.get('.react-flow__edge').should('have.length', 0);
// }); });
it('connects nodes', () => { it('connects nodes', () => {
cy.get('.react-flow__node') cy.get('.react-flow__node')
@@ -125,16 +120,15 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
.trigger('mouseup', { force: true, button: 0 }); .trigger('mouseup', { force: true, button: 0 });
cy.get('.react-flow__edge').as('edge'); cy.get('.react-flow__edge').as('edge');
cy.get('@edge').should('have.length', 3); cy.get('@edge').should('have.length', 1);
}); });
// @TODO: why does this fail since react18? it('removes an edge', () => {
// it('removes an edge', () => { cy.get('.react-flow__edge:first').click();
// cy.get('.react-flow__edge:first').click(); cy.get('html').realPress('Backspace');
// cy.get('body').type('{backspace}');
// cy.get('.react-flow__edge').should('have.length', 1); cy.get('.react-flow__edge').should('have.length', 0);
// }); });
it('drags the pane', () => { it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform'); const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
@@ -59,7 +59,7 @@ function ControlledFlow({
handlers.onConnect = onConnect; handlers.onConnect = onConnect;
} }
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} />; return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} nodeDragThreshold={0} />;
} }
export default ControlledFlow; export default ControlledFlow;
+1 -1
View File
@@ -9,7 +9,7 @@ Cypress.Commands.add('drag', (selector, { x, y }) =>
const nextY: number = centerY + y; const nextY: number = centerY + y;
return elementToDrag return elementToDrag
.trigger('mousedown', { view: window }) .trigger('mousedown', { view: window, force: true })
.trigger('mousemove', nextX, nextY, { force: true }) .trigger('mousemove', nextX, nextY, { force: true })
.wait(50) .wait(50)
.trigger('mouseup', { view: window, force: true }); .trigger('mouseup', { view: window, force: true });
+1
View File
@@ -15,6 +15,7 @@
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands'; import './commands';
import 'cypress-real-events/support';
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/; const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Flow Examples</title> <title>React Flow Examples</title>
</head> </head>
+17 -14
View File
@@ -1,5 +1,5 @@
{ {
"name": "@xyflow/react-examples", "name": "react-examples",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
@@ -14,26 +14,29 @@
"test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'" "test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.2.3",
"@xyflow/react": "workspace:*", "@xyflow/react": "workspace:*",
"classcat": "^5.0.3", "classcat": "^5.0.4",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.3.0", "react-redux": "^9.1.1",
"zustand": "^4.4.1" "react-router-dom": "^6.18.0",
"redux": "^5.0.1",
"zustand": "^4.4.6"
}, },
"devDependencies": { "devDependencies": {
"@cypress/skip-test": "^2.6.1", "@cypress/skip-test": "^2.6.1",
"@types/dagre": "^0.7.48", "@types/dagre": "^0.7.52",
"@types/react": "^18.0.17", "@types/react": "^18.2.36",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.2.14",
"@vitejs/plugin-react": "4.0.4", "@vitejs/plugin-react": "4.1.1",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.4.1",
"cypress": "12.17.3", "cypress": "13.6.6",
"cypress-real-events": "1.10.0", "cypress-real-events": "1.12.0",
"start-server-and-test": "^1.14.0", "start-server-and-test": "^2.0.2",
"typescript": "5.1.3", "typescript": "5.2.2",
"vite": "4.4.9" "vite": "4.5.0"
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+34
View File
@@ -0,0 +1,34 @@
import { useEffect, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import routes from './routes';
export default function Header() {
const location = useLocation();
const navigate = useNavigate();
const [currentPath, setCurrentPath] = useState(location.pathname);
useEffect(() => {
const name = routes.find((route) => route.path === currentPath)?.name;
document.title = `React Flow Examples${name ? ' - ' + name : ''}`;
navigate(currentPath);
}, [currentPath]);
return (
<>
<header>
<a className="logo" href="https://github.com/xyflow/xyflow">
React Flow Dev
</a>
<select value={currentPath} onChange={(event) => setCurrentPath(event.target.value)}>
{routes.map((route) => (
<option value={route.path} key={route.path}>
{route.name}
</option>
))}
</select>
</header>
<Outlet />
</>
);
}
+12 -317
View File
@@ -1,326 +1,21 @@
import { useEffect, useState } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import routes from './routes';
import GenericTestRoutes from '../generic-tests';
import Basic from '../examples/Basic'; import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds'; import Header from './header';
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
import ControlledViewport from '../examples/ControlledViewport';
import CustomConnectionLine from '../examples/CustomConnectionLine';
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
import CustomNode from '../examples/CustomNode';
import DefaultNodes from '../examples/DefaultNodes';
import DragHandle from '../examples/DragHandle';
import DragNDrop from '../examples/DragNDrop';
import EasyConnect from '../examples/EasyConnect';
import Edges from '../examples/Edges';
import EdgeRenderer from '../examples/EdgeRenderer';
import EdgeTypes from '../examples/EdgeTypes';
import Empty from '../examples/Empty';
import Figma from '../examples/Figma';
import FloatingEdges from '../examples/FloatingEdges';
import Hidden from '../examples/Hidden';
import Interaction from '../examples/Interaction';
import Intersection from '../examples/Intersection';
import Layouting from '../examples/Layouting';
import MultiFlows from '../examples/MultiFlows';
import NestedNodes from '../examples/NestedNodes';
import NodeResizer from '../examples/NodeResizer';
import NodeTypeChange from '../examples/NodeTypeChange';
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
import Overview from '../examples/Overview';
import Provider from '../examples/Provider';
import SaveRestore from '../examples/SaveRestore';
import Stress from '../examples/Stress';
import Subflow from '../examples/Subflow';
import SwitchFlow from '../examples/Switch';
import TouchDevice from '../examples/TouchDevice';
import Undirectional from '../examples/Undirectional';
import UpdatableEdge from '../examples/UpdatableEdge';
import UpdateNode from '../examples/UpdateNode';
import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals';
import UseReactFlow from '../examples/UseReactFlow';
import Validation from '../examples/Validation';
import UseKeyPress from '../examples/UseKeyPress';
import EdgeRouting from '../examples/EdgeRouting';
import CancelConnection from '../examples/CancelConnection';
import InteractiveMinimap from '../examples/InteractiveMinimap';
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
import NodeToolbar from '../examples/NodeToolbar';
import useNodesInitialized from '../examples/UseNodesInit';
interface IRoute {
name: string;
path: string;
component: React.ComponentType;
}
const routes: IRoute[] = [
{
name: 'Basic',
path: '/',
component: Basic,
},
{
name: 'Backgrounds',
path: '/backgrounds',
component: Backgrounds,
},
{
name: 'Cancel Connection',
path: '/cancel-connection',
component: CancelConnection,
},
{
name: 'Controlled/Uncontrolled',
path: '/controlled-uncontrolled',
component: ControlledUncontrolled,
},
{
name: 'Controlled Viewport',
path: '/controlled-viewport',
component: ControlledViewport,
},
{
name: 'Custom Connection Line',
path: '/custom-connectionline',
component: CustomConnectionLine,
},
{
name: 'Custom Minimap Node',
path: '/custom-minimap-node',
component: CustomMiniMapNode,
},
{
name: 'Custom Node',
path: '/custom-node',
component: CustomNode,
},
{
name: 'Default Nodes',
path: '/default-nodes',
component: DefaultNodes,
},
{
name: 'Drag Handle',
path: '/draghandle',
component: DragHandle,
},
{
name: 'Drag and Drop',
path: '/dragndrop',
component: DragNDrop,
},
{
name: 'EasyConnect',
path: '/easy-connect',
component: EasyConnect,
},
{
name: 'Edges',
path: '/edges',
component: Edges,
},
{
name: 'Edge Renderer',
path: '/edge-renderer',
component: EdgeRenderer,
},
{
name: 'Edge Types',
path: '/edge-types',
component: EdgeTypes,
},
{
name: 'Edge Routing',
path: '/edge-routing',
component: EdgeRouting,
},
{
name: 'Empty',
path: '/empty',
component: Empty,
},
{
name: 'Figma',
path: '/figma',
component: Figma,
},
{
name: 'Floating Edges',
path: '/floating-edges',
component: FloatingEdges,
},
{
name: 'Hidden',
path: '/hidden',
component: Hidden,
},
{
name: 'Interaction',
path: '/interaction',
component: Interaction,
},
{
name: 'Intersection',
path: '/intersection',
component: Intersection,
},
{
name: 'Interactive Minimap',
path: '/interactive-minimap',
component: InteractiveMinimap,
},
{
name: 'Layouting',
path: '/layouting',
component: Layouting,
},
{
name: 'Multi Flows',
path: '/multiflows',
component: MultiFlows,
},
{
name: 'Nested Nodes',
path: '/nested-nodes',
component: NestedNodes,
},
{
name: 'Node Type Change',
path: '/nodetype-change',
component: NodeTypeChange,
},
{
name: 'nodeTypes Object Change',
path: '/nodetypesobject-change',
component: NodeTypesObjectChange,
},
{
name: 'NodeToolbar',
path: '/node-toolbar',
component: NodeToolbar,
},
{
name: 'NodeResizer',
path: '/node-resizer',
component: NodeResizer,
},
{
name: 'Overview',
path: '/overview',
component: Overview,
},
{
name: 'Provider',
path: '/provider',
component: Provider,
},
{
name: 'Save/Restore',
path: '/save-restore',
component: SaveRestore,
},
{
name: 'Stress',
path: '/stress',
component: Stress,
},
{
name: 'Subflow',
path: '/subflow',
component: Subflow,
},
{
name: 'Switch Flow',
path: '/switch',
component: SwitchFlow,
},
{
name: 'Touch Device',
path: '/touch-device',
component: TouchDevice,
},
{
name: 'Undirectional',
path: '/undirectional',
component: Undirectional,
},
{
name: 'Updatable Edge',
path: '/updatable-edge',
component: UpdatableEdge,
},
{
name: 'Update Node',
path: '/update-node',
component: UpdateNode,
},
{
name: 'useNodesInitialized',
path: '/use-nodes-initialized',
component: useNodesInitialized,
},
{
name: 'useOnSelectionChange',
path: '/use-on-selection-change',
component: UseOnSelectionChange,
},
{
name: 'useReactFlow',
path: '/usereactflow',
component: UseReactFlow,
},
{
name: 'useUpdateNodeInternals',
path: '/useupdatenodeinternals',
component: UseUpdateNodeInternals,
},
{
name: 'Validation',
path: '/validation',
component: Validation,
},
{
name: 'useKeyPress',
path: '/use-key-press',
component: UseKeyPress,
},
];
const Header = () => {
const location = useLocation();
const navigate = useNavigate();
const [currentPath, setCurrentPath] = useState(location.pathname);
useEffect(() => {
const name = routes.find((route) => route.path === currentPath)?.name;
document.title = `React Flow Examples${name ? ' - ' + name : ''}`;
navigate(currentPath);
}, [currentPath]);
return (
<header>
<a className="logo" href="https://github.com/wbkd/react-flow">
React Flow Dev
</a>
<select value={currentPath} onChange={(event) => setCurrentPath(event.target.value)}>
{routes.map((route) => (
<option value={route.path} key={route.path}>
{route.name}
</option>
))}
</select>
</header>
);
};
export default () => ( export default () => (
<BrowserRouter> <BrowserRouter>
<Header />
<Routes> <Routes>
{routes.map((route) => ( <Route path="/" element={<Navigate to="/examples" />} />
<Route path={route.path} key={route.path} element={<route.component />} /> <Route path="examples" element={<Header />}>
))} <Route index element={<Basic />} />
{routes.map((route) => (
<Route path={route.path} key={route.path} element={<route.component />} />
))}
</Route>
<Route path="tests/generic/*" element={<GenericTestRoutes />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
); );
+335
View File
@@ -0,0 +1,335 @@
import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds';
import ColorMode from '../examples/ColorMode';
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
import ControlledViewport from '../examples/ControlledViewport';
import CustomConnectionLine from '../examples/CustomConnectionLine';
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
import CustomNode from '../examples/CustomNode';
import DefaultNodes from '../examples/DefaultNodes';
import DragHandle from '../examples/DragHandle';
import DragNDrop from '../examples/DragNDrop';
import EasyConnect from '../examples/EasyConnect';
import Edges from '../examples/Edges';
import EdgeRenderer from '../examples/EdgeRenderer';
import EdgeTypes from '../examples/EdgeTypes';
import Empty from '../examples/Empty';
import Figma from '../examples/Figma';
import FloatingEdges from '../examples/FloatingEdges';
import Hidden from '../examples/Hidden';
import Interaction from '../examples/Interaction';
import Intersection from '../examples/Intersection';
import Layouting from '../examples/Layouting';
import MultiFlows from '../examples/MultiFlows';
import MultiSetNodes from '../examples/MultiSetNodes';
import NodeResizer from '../examples/NodeResizer';
import NodeTypeChange from '../examples/NodeTypeChange';
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
import Overview from '../examples/Overview';
import Provider from '../examples/Provider';
import SaveRestore from '../examples/SaveRestore';
import SetNodesBatching from '../examples/SetNodesBatching';
import Stress from '../examples/Stress';
import Subflow from '../examples/Subflow';
import SwitchFlow from '../examples/Switch';
import TouchDevice from '../examples/TouchDevice';
import Undirectional from '../examples/Undirectional';
import ReconnectEdge from '../examples/ReconnectEdge';
import UpdateNode from '../examples/UpdateNode';
import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals';
import UseReactFlow from '../examples/UseReactFlow';
import Validation from '../examples/Validation';
import UseKeyPress from '../examples/UseKeyPress';
import EdgeRouting from '../examples/EdgeRouting';
import CancelConnection from '../examples/CancelConnection';
import InteractiveMinimap from '../examples/InteractiveMinimap';
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
import NodeToolbar from '../examples/NodeToolbar';
import UseConnection from '../examples/UseConnection';
import UseNodesInitialized from '../examples/UseNodesInit';
import UseNodesData from '../examples/UseNodesData';
import UseHandleConnections from '../examples/UseHandleConnections';
import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop';
import DevTools from '../examples/DevTools';
import Redux from '../examples/Redux';
export interface IRoute {
name: string;
path: string;
component: React.ComponentType;
}
const routes: IRoute[] = [
{
name: 'Add Node on edge Drop',
path: 'add-node-edge-drop',
component: AddNodeOnEdgeDrop,
},
{
name: 'Basic',
path: 'basic',
component: Basic,
},
{
name: 'Backgrounds',
path: 'backgrounds',
component: Backgrounds,
},
{
name: 'Color Mode',
path: 'color-mode',
component: ColorMode,
},
{
name: 'Cancel Connection',
path: 'cancel-connection',
component: CancelConnection,
},
{
name: 'Controlled/Uncontrolled',
path: 'controlled-uncontrolled',
component: ControlledUncontrolled,
},
{
name: 'Controlled Viewport',
path: 'controlled-viewport',
component: ControlledViewport,
},
{
name: 'Custom Connection Line',
path: 'custom-connectionline',
component: CustomConnectionLine,
},
{
name: 'Custom Minimap Node',
path: 'custom-minimap-node',
component: CustomMiniMapNode,
},
{
name: 'Custom Node',
path: 'custom-node',
component: CustomNode,
},
{
name: 'Default Nodes',
path: 'default-nodes',
component: DefaultNodes,
},
{
name: 'DevTools',
path: 'devtools',
component: DevTools,
},
{
name: 'Drag Handle',
path: 'draghandle',
component: DragHandle,
},
{
name: 'Drag and Drop',
path: 'dragndrop',
component: DragNDrop,
},
{
name: 'EasyConnect',
path: 'easy-connect',
component: EasyConnect,
},
{
name: 'Edges',
path: 'edges',
component: Edges,
},
{
name: 'Edge Renderer',
path: 'edge-renderer',
component: EdgeRenderer,
},
{
name: 'Edge Types',
path: 'edge-types',
component: EdgeTypes,
},
{
name: 'Edge Routing',
path: 'edge-routing',
component: EdgeRouting,
},
{
name: 'Empty',
path: 'empty',
component: Empty,
},
{
name: 'Figma',
path: 'figma',
component: Figma,
},
{
name: 'Floating Edges',
path: 'floating-edges',
component: FloatingEdges,
},
{
name: 'Hidden',
path: 'hidden',
component: Hidden,
},
{
name: 'Interaction',
path: 'interaction',
component: Interaction,
},
{
name: 'Intersection',
path: 'intersection',
component: Intersection,
},
{
name: 'Interactive Minimap',
path: 'interactive-minimap',
component: InteractiveMinimap,
},
{
name: 'Layouting',
path: 'layouting',
component: Layouting,
},
{
name: 'Multi setNodes',
path: 'multi-setnodes',
component: MultiSetNodes,
},
{
name: 'Multi Flows',
path: 'multiflows',
component: MultiFlows,
},
{
name: 'Node Type Change',
path: 'nodetype-change',
component: NodeTypeChange,
},
{
name: 'nodeTypes Object Change',
path: 'nodetypesobject-change',
component: NodeTypesObjectChange,
},
{
name: 'NodeToolbar',
path: 'node-toolbar',
component: NodeToolbar,
},
{
name: 'NodeResizer',
path: 'node-resizer',
component: NodeResizer,
},
{
name: 'Overview',
path: 'overview',
component: Overview,
},
{
name: 'Provider',
path: 'provider',
component: Provider,
},
{
name: 'Save/Restore',
path: 'save-restore',
component: SaveRestore,
},
{
name: 'SetNodes Batching',
path: 'setnodes-batching',
component: SetNodesBatching,
},
{
name: 'Stress',
path: 'stress',
component: Stress,
},
{
name: 'Subflow',
path: 'subflow',
component: Subflow,
},
{
name: 'Switch Flow',
path: 'switch',
component: SwitchFlow,
},
{
name: 'Touch Device',
path: 'touch-device',
component: TouchDevice,
},
{
name: 'Undirectional',
path: 'undirectional',
component: Undirectional,
},
{
name: 'Reconnect Edge',
path: 'reconnect-edge',
component: ReconnectEdge,
},
{
name: 'Update Node',
path: 'update-node',
component: UpdateNode,
},
{
name: 'useConnection',
path: 'use-connection',
component: UseConnection,
},
{
name: 'useNodesInitialized',
path: 'use-nodes-initialized',
component: UseNodesInitialized,
},
{
name: 'useOnSelectionChange',
path: 'use-on-selection-change',
component: UseOnSelectionChange,
},
{
name: 'useReactFlow',
path: 'usereactflow',
component: UseReactFlow,
},
{
name: 'useHandleConnections',
path: 'usehandleconnections',
component: UseHandleConnections,
},
{
name: 'useNodesData',
path: 'usenodesdata',
component: UseNodesData,
},
{
name: 'useUpdateNodeInternals',
path: 'useupdatenodeinternals',
component: UseUpdateNodeInternals,
},
{
name: 'redux',
path: 'redux',
component: Redux,
},
{
name: 'Validation',
path: 'validation',
component: Validation,
},
{
name: 'useKeyPress',
path: 'use-key-press',
component: UseKeyPress,
},
];
export default routes;
+11
View File
@@ -0,0 +1,11 @@
import { BackgroundProps, ControlProps, Edge, MiniMapProps, Node, PanelProps, ReactFlowProps } from '@xyflow/react';
declare global {
interface FlowConfig {
flowProps?: Omit<ReactFlowProps, 'nodes' | 'edges'> & { nodes: Node[]; edges: Edge[] };
panelProps?: PanelProps;
backgroundProps?: BackgroundProps;
controlsProps?: ControlProps;
minimapProps?: MiniMapProps;
}
}
@@ -0,0 +1,98 @@
import { useCallback, useRef } from 'react';
import {
ReactFlow,
useNodesState,
useEdgesState,
addEdge,
useReactFlow,
ReactFlowProvider,
OnConnect,
OnConnectStart,
OnConnectEnd,
Node,
Edge,
} from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '0',
type: 'input',
data: { label: 'Node' },
position: { x: 0, y: 50 },
},
];
const initialEdges: Edge[] = [];
let id = 1;
const getId = () => `${id++}`;
const AddNodeOnEdgeDrop = () => {
const reactFlowWrapper = useRef(null);
const connectingNodeId = useRef<string | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const { screenToFlowPosition } = useReactFlow();
const onConnect: OnConnect = useCallback((params) => {
// reset the start node on connections
connectingNodeId.current = null;
setEdges((eds) => addEdge(params, eds));
}, []);
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
if (!connectingNodeId.current) return;
const targetIsPane = (event.target as HTMLDivElement)?.classList.contains('react-flow__pane');
if (targetIsPane && 'clientX' in event && 'clientY' in event) {
// we need to remove the wrapper bounds, in order to get the correct position
const id = getId();
const newNode: Node = {
id,
position: screenToFlowPosition({
x: event.clientX,
y: event.clientY,
}),
data: { label: `Node ${id}` },
origin: [0.5, 0.0],
};
const newEdge: Edge = {
id,
source: connectingNodeId.current,
target: id,
};
setNodes((nds) => nds.concat(newNode));
setEdges((eds) => eds.concat(newEdge));
}
},
[screenToFlowPosition]
);
return (
<div className="wrapper" ref={reactFlowWrapper} style={{ height: '100%' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
fitView
/>
</div>
);
};
export default () => (
<ReactFlowProvider>
<AddNodeOnEdgeDrop />
</ReactFlowProvider>
);
+66 -16
View File
@@ -1,4 +1,4 @@
import { MouseEvent } from 'react'; import { MouseEvent, useCallback } from 'react';
import { import {
ReactFlow, ReactFlow,
MiniMap, MiniMap,
@@ -10,12 +10,16 @@ import {
Edge, Edge,
useReactFlow, useReactFlow,
Panel, Panel,
OnNodeDrag,
} from '@xyflow/react'; } from '@xyflow/react';
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node); const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStart = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const printSelectionEvent = (name: string) => (_: MouseEvent, nodes: Node[]) => console.log(name, nodes);
const initialNodes: Node[] = [ const initialNodes: Node[] = [
{ {
id: '1', id: '1',
@@ -52,41 +56,80 @@ const initialEdges: Edge[] = [
const defaultEdgeOptions = {}; const defaultEdgeOptions = {};
const BasicFlow = () => { const BasicFlow = () => {
const instance = useReactFlow(); const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
useReactFlow();
const updatePos = () => { const updatePos = () => {
instance.setNodes((nodes) => setNodes((nodes) =>
nodes.map((node) => { nodes.map((node) => {
node.position = { return {
x: Math.random() * 400, ...node,
y: Math.random() * 400, position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
}; };
return node;
}) })
); );
}; };
const logToObject = () => console.log(instance.toObject()); const logToObject = () => console.log(toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 }); const resetTransform = () => setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => { const toggleClassnames = () => {
instance.setNodes((nodes) => setNodes((nodes) =>
nodes.map((node) => { nodes.map((node) => {
node.className = node.className === 'light' ? 'dark' : 'light'; return {
...node,
return node; className: node.className === 'light' ? 'dark' : 'light',
};
}) })
); );
}; };
const deleteSelectedElements = useCallback(() => {
const selectedNodes = getNodes().filter((node) => node.selected);
const selectedEdges = getEdges().filter((edge) => edge.selected);
deleteElements({ nodes: selectedNodes, edges: selectedEdges });
}, [deleteElements]);
const deleteSomeElements = useCallback(() => {
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
}, []);
const onSetNodes = () => {
setNodes([
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node a' } },
{ id: 'b', position: { x: 0, y: 150 }, data: { label: 'Node b' } },
]);
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
};
const onUpdateNode = () => {
updateNodeData('1', { label: 'update' });
updateNodeData('2', { label: 'update' });
};
const addNode = () => {
addNodes({
id: `${Math.random()}`,
data: { label: 'Node' },
position: { x: Math.random() * 300, y: Math.random() * 300 },
className: 'light',
});
};
return ( return (
<ReactFlow <ReactFlow
defaultNodes={initialNodes} defaultNodes={initialNodes}
defaultEdges={initialEdges} defaultEdges={initialEdges}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag} onNodeDrag={onNodeDrag}
onSelectionDragStart={printSelectionEvent('selection drag start')}
onSelectionDrag={printSelectionEvent('selection drag')}
onSelectionDragStop={printSelectionEvent('selection drag stop')}
className="react-flow-basic-example" className="react-flow-basic-example"
minZoom={0.2} minZoom={0.2}
maxZoom={4} maxZoom={4}
@@ -95,6 +138,7 @@ const BasicFlow = () => {
selectNodesOnDrag={false} selectNodesOnDrag={false}
elevateEdgesOnSelect elevateEdgesOnSelect
elevateNodesOnSelect={false} elevateNodesOnSelect={false}
nodeDragThreshold={0}
> >
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -105,6 +149,12 @@ const BasicFlow = () => {
<button onClick={updatePos}>change pos</button> <button onClick={updatePos}>change pos</button>
<button onClick={toggleClassnames}>toggle classnames</button> <button onClick={toggleClassnames}>toggle classnames</button>
<button onClick={logToObject}>toObject</button> <button onClick={logToObject}>toObject</button>
<button onClick={deleteSelectedElements}>deleteSelectedElements</button>
<button onClick={deleteSomeElements}>deleteSomeElements</button>
<button onClick={onSetNodes}>setNodes</button>
<button onClick={onUpdateNode}>updateNode</button>
<button onClick={addNode}>addNode</button>
</Panel> </Panel>
</ReactFlow> </ReactFlow>
); );
@@ -0,0 +1,90 @@
import { ChangeEventHandler, useCallback, useState } from 'react';
import {
ReactFlow,
addEdge,
Node,
useNodesState,
useEdgesState,
OnConnect,
Edge,
MiniMap,
Background,
Controls,
Panel,
ColorMode,
Position,
} from '@xyflow/react';
import './style.css';
const nodeDefaults = {
sourcePosition: Position.Right,
targetPosition: Position.Left,
};
const initialNodes: Node[] = [
{ id: 'A', type: 'input', position: { x: 0, y: 150 }, data: { label: 'A' }, ...nodeDefaults },
{ id: 'B', position: { x: 250, y: 0 }, data: { label: 'B' }, ...nodeDefaults },
{ id: 'C', position: { x: 250, y: 150 }, data: { label: 'C' }, ...nodeDefaults },
{ id: 'D', position: { x: 250, y: 300 }, data: { label: 'D' }, ...nodeDefaults },
];
const initialEdges: Edge[] = [
{
id: 'A-B',
source: 'A',
target: 'B',
},
{
id: 'A-C',
source: 'A',
target: 'C',
},
{
id: 'A-D',
source: 'A',
target: 'D',
},
];
const ColorModeFlow = () => {
const [colorMode, setColorMode] = useState<ColorMode>('light');
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect: OnConnect = useCallback(
(params) => {
console.log('on connect', params);
setEdges((eds) => addEdge(params, eds));
},
[setEdges]
);
const onChange: ChangeEventHandler<HTMLSelectElement> = (evt) => setColorMode(evt.target.value as ColorMode);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
colorMode={colorMode}
fitView
>
<MiniMap />
<Background />
<Controls />
<Panel position="top-right">
<select onChange={onChange} data-testid="colormode-select">
<option value="light">light</option>
<option value="dark">dark</option>
<option value="system">system</option>
</select>
</Panel>
</ReactFlow>
);
};
export default ColorModeFlow;
@@ -60,12 +60,13 @@ const ControlledUncontrolled = () => {
const updateNodePositions = () => { const updateNodePositions = () => {
instance.setNodes((nodes) => instance.setNodes((nodes) =>
nodes.map((node) => { nodes.map((node) => {
node.position = { return {
x: Math.random() * 400, ...node,
y: Math.random() * 400, position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
}; };
return node;
}) })
); );
}; };
@@ -73,11 +74,12 @@ const ControlledUncontrolled = () => {
const updateEdgeColors = () => { const updateEdgeColors = () => {
instance.setEdges((edges) => instance.setEdges((edges) =>
edges.map((edge) => { edges.map((edge) => {
edge.style = { return {
stroke: '#ff5050', ...edge,
style: {
stroke: '#ff5050',
},
}; };
return edge;
}) })
); );
}; };
@@ -27,13 +27,13 @@ const buttonStyle: CSSProperties = {
zIndex: 4, zIndex: 4,
}; };
const CustomMiniMapNode = ({ x, y, width, height, color }: MiniMapNodeProps) => ( const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => {
<circle cx={x} cy={y} r={Math.max(width, height) / 2} fill={color} /> return <circle cx={x} cy={y} r={Math.max(width, height) / 2} fill="#ffcc00" />;
); };
const CustomMiniMapNodeFlow = () => { const CustomMiniMapNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]); const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
const addRandomNode = () => { const addRandomNode = () => {
@@ -59,7 +59,7 @@ const CustomMiniMapNodeFlow = () => {
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onConnect={(p) => onConnect(p)} onConnect={(p) => onConnect(p)}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onlyRenderVisibleElements={false} onlyRenderVisibleElements={true}
> >
<Controls /> <Controls />
<Background variant={BackgroundVariant.Lines} /> <Background variant={BackgroundVariant.Lines} />
@@ -1,6 +1,8 @@
import React, { memo, FC, CSSProperties, useCallback } from 'react'; import React, { memo, CSSProperties, useCallback } from 'react';
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react'; import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react';
import type { ColorSelectorNode } from '.';
const targetHandleStyle: CSSProperties = { background: '#555' }; const targetHandleStyle: CSSProperties = { background: '#555' };
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 }; const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
const sourceHandleStyleB: CSSProperties = { const sourceHandleStyleB: CSSProperties = {
@@ -11,7 +13,7 @@ const sourceHandleStyleB: CSSProperties = {
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params); const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
const ColorSelectorNode: FC<NodeProps> = ({ data, isConnectable }) => { function ColorSelectorNode({ data, isConnectable }: NodeProps<ColorSelectorNode>) {
const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []); const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []);
const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []); const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []);
const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []); const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []);
@@ -42,6 +44,6 @@ const ColorSelectorNode: FC<NodeProps> = ({ data, isConnectable }) => {
<Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} /> <Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} />
</> </>
); );
}; }
export default memo(ColorSelectorNode); export default memo(ColorSelectorNode);
@@ -1,40 +1,64 @@
import { useState, useEffect, MouseEvent, ChangeEvent, useCallback } from 'react'; import { useState, useEffect, MouseEvent, ChangeEvent, useCallback, useRef } from 'react';
import { import {
ReactFlow, ReactFlow,
MiniMap, MiniMap,
Controls, Controls,
addEdge, addEdge,
Node, Node,
ReactFlowInstance,
Position, Position,
SnapGrid, SnapGrid,
Connection,
useNodesState,
useEdgesState, useEdgesState,
Background, Background,
OnNodeDrag,
OnInit,
applyNodeChanges,
OnNodesChange,
OnConnect,
OnBeforeDelete,
BuiltInNode,
BuiltInEdge,
NodeTypes,
ReactFlowProvider,
} from '@xyflow/react'; } from '@xyflow/react';
import ColorSelectorNode from './ColorSelectorNode'; import ColorSelectorNode from './ColorSelectorNode';
const onInit = (reactFlowInstance: ReactFlowInstance) => { export type ColorSelectorNode = Node<
{ color: string; onChange: (event: ChangeEvent<HTMLInputElement>) => void },
'selectorNode'
>;
export type MyNode = BuiltInNode | ColorSelectorNode;
export type MyEdge = BuiltInEdge;
const onInit: OnInit<MyNode, MyEdge> = (reactFlowInstance) => {
console.log('flow loaded:', reactFlowInstance); console.log('flow loaded:', reactFlowInstance);
}; };
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStop: OnNodeDrag<MyNode> = (_, node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeClick = (_: MouseEvent, node: MyNode) => console.log('click', node);
const initBgColor = '#1A192B'; const initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' }; const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16]; const snapGrid: SnapGrid = [16, 16];
const nodeTypes = { const nodeTypes: NodeTypes = {
selectorNode: ColorSelectorNode, selectorNode: ColorSelectorNode,
}; };
const CustomNodeFlow = () => { const CustomNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]); const ref = useRef(null);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [nodes, setNodes] = useState<MyNode[]>([]);
const onNodesChange: OnNodesChange<MyNode> = useCallback(
(changes) =>
setNodes((nds) => {
const nextNodes = applyNodeChanges(changes, nds);
return nextNodes;
}),
[setNodes]
);
const [edges, setEdges, onEdgesChange] = useEdgesState<MyEdge>([]);
const [bgColor, setBgColor] = useState<string>(initBgColor); const [bgColor, setBgColor] = useState<string>(initBgColor);
@@ -42,7 +66,7 @@ const CustomNodeFlow = () => {
const onChange = (event: ChangeEvent<HTMLInputElement>) => { const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setNodes((nds) => setNodes((nds) =>
nds.map((node) => { nds.map((node) => {
if (node.id !== '2') { if (node.id !== '2' || node.type !== 'selectorNode') {
return node; return node;
} }
@@ -119,12 +143,13 @@ const CustomNodeFlow = () => {
]); ]);
}, []); }, []);
const onConnect = useCallback( const onConnect: OnConnect = useCallback(
(connection: Connection) => (connection) => setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
[setEdges] [setEdges]
); );
const onBeforeDelete: OnBeforeDelete<MyNode, MyEdge> = useCallback(async (params) => true, []);
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -142,16 +167,18 @@ const CustomNodeFlow = () => {
fitView fitView
minZoom={0.3} minZoom={0.3}
maxZoom={2} maxZoom={2}
onBeforeDelete={onBeforeDelete}
ref={ref}
> >
<MiniMap <MiniMap<MyNode>
nodeStrokeColor={(n: Node): string => { nodeStrokeColor={(n: MyNode): string => {
if (n.type === 'input') return '#0041d0'; if (n.type === 'input') return '#0041d0';
if (n.type === 'selectorNode') return bgColor; if (n.type === 'selectorNode') return bgColor;
if (n.type === 'output') return '#ff0072'; if (n.type === 'output') return '#ff0072';
return '#eee'; return '#eee';
}} }}
nodeColor={(n: Node): string => { nodeColor={(n: MyNode): string => {
if (n.type === 'selectorNode') return bgColor; if (n.type === 'selectorNode') return bgColor;
return '#fff'; return '#fff';
@@ -163,4 +190,8 @@ const CustomNodeFlow = () => {
); );
}; };
export default CustomNodeFlow; export default () => (
<ReactFlowProvider>
<CustomNodeFlow />
</ReactFlowProvider>
);
@@ -25,6 +25,7 @@ const defaultNodes: Node[] = [
}, },
{ {
id: '3', id: '3',
type: 'output',
data: { label: 'Node 3' }, data: { label: 'Node 3' },
position: { x: 400, y: 100 }, position: { x: 400, y: 100 },
className: 'light', className: 'light',
@@ -54,26 +55,24 @@ const DefaultNodes = () => {
const updateNodePositions = () => { const updateNodePositions = () => {
instance.setNodes((nodes) => instance.setNodes((nodes) =>
nodes.map((node) => { nodes.map((node) => ({
node.position = { ...node,
position: {
x: Math.random() * 400, x: Math.random() * 400,
y: Math.random() * 400, y: Math.random() * 400,
}; },
}))
return node;
})
); );
}; };
const updateEdgeColors = () => { const updateEdgeColors = () => {
instance.setEdges((edges) => instance.setEdges((edges) =>
edges.map((edge) => { edges.map((edge) => ({
edge.style = { ...edge,
style: {
stroke: '#ff5050', stroke: '#ff5050',
}; },
}))
return edge;
})
); );
}; };
@@ -0,0 +1,74 @@
import { useEffect, useRef, useState } from 'react';
import { NodeChange, OnNodesChange, useStore, useStoreApi } from '@xyflow/react';
type ChangeLoggerProps = {
color?: string;
limit?: number;
};
type ChangeInfoProps = {
change: NodeChange;
};
function ChangeInfo({ change }: ChangeInfoProps) {
const id = 'id' in change ? change.id : '-';
const { type } = change;
return (
<div style={{ marginBottom: 4 }}>
<div>node id: {id}</div>
<div>
{type === 'add' ? JSON.stringify(change.item, null, 2) : null}
{type === 'dimensions' ? `${change.dimensions?.width} × ${change.dimensions?.height}` : null}
{type === 'position' ? `position: ${change.position?.x.toFixed(1)}, ${change.position?.y.toFixed(1)}` : null}
{type === 'remove' ? 'remove' : null}
{type === 'replace' ? JSON.stringify(change.item, null, 2) : null}
{type === 'select' ? (change.selected ? 'select' : 'unselect') : null}
</div>
</div>
);
}
export default function ChangeLogger({ limit = 20 }: ChangeLoggerProps) {
const [changes, setChanges] = useState<NodeChange[]>([]);
const onNodesChangeIntercepted = useRef(false);
const onNodesChange = useStore((s) => s.onNodesChange);
const store = useStoreApi();
useEffect(() => {
if (!onNodesChange || onNodesChangeIntercepted.current) {
return;
}
onNodesChangeIntercepted.current = true;
const userOnNodesChange = onNodesChange;
const onNodesChangeLogger: OnNodesChange = (changes) => {
userOnNodesChange(changes);
setChanges((c) => {
changes.forEach((change) => {
if (c.length >= limit) {
c.pop();
}
c = [change, ...c];
});
return c;
});
};
store.setState({ onNodesChange: onNodesChangeLogger });
}, [onNodesChange]);
return (
<div className="react-flow__devtools-changelogger">
<div className="react-flow__devtools-title">Change Logger</div>
{changes.length === 0 ? (
<>no changes triggered</>
) : (
changes.map((change, index) => <ChangeInfo key={index} change={change} />)
)}
</div>
);
}
@@ -0,0 +1,68 @@
import { useNodes, ViewportPortal } from '@xyflow/react';
type NodeInfoProps = {
id: string;
type: string;
x: number;
y: number;
width?: number;
height?: number;
data: any;
};
function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) {
if (!width || !height) {
return null;
}
return (
<div
className="react-flow__devtools-nodeinfo"
style={{
position: 'absolute',
transform: `translate(${x}px, ${y + height}px)`,
width: width * 2,
}}
>
<div>id: {id}</div>
<div>type: {type}</div>
<div>
position: {x.toFixed(1)}, {y.toFixed(1)}
</div>
<div>
dimensions: {width} × {height}
</div>
<div>data: {JSON.stringify(data, null, 2)}</div>
</div>
);
}
export default function NodeInspector() {
const nodes = useNodes();
return (
<ViewportPortal>
<div className="react-flow__devtools-nodeinspector">
{nodes.map((node) => {
const x = node?.position?.x || 0;
const y = node?.position?.y || 0;
const width = node.measured?.width || 0;
const height = node.measured?.height || 0;
return (
<NodeInfo
key={node.id}
id={node.id}
type={node.type || 'default'}
x={x}
y={y}
width={width}
height={height}
data={node.data}
/>
);
})}
</div>
</ViewportPortal>
);
}
@@ -0,0 +1,44 @@
import { useState, type Dispatch, type SetStateAction, type ReactNode, HTMLAttributes } from 'react';
import { Panel, PanelPosition } from '@xyflow/react';
import NodeInspector from './NodeInspector';
import ChangeLogger from './ChangeLogger';
import './style.css';
export default function ReactFlowDevTools({ position = 'top-left' }: { position?: PanelPosition }) {
const [nodeInspectorActive, setNodeInspectorActive] = useState(false);
const [changeLoggerActive, setChangeLoggerActive] = useState(false);
return (
<div className="react-flow__devtools">
<Panel position={position}>
<DevToolButton setActive={setNodeInspectorActive} active={nodeInspectorActive} title="Toggle Node Inspector">
Node Inspector
</DevToolButton>
<DevToolButton setActive={setChangeLoggerActive} active={changeLoggerActive} title="Toggle Change Logger">
Change Logger
</DevToolButton>
</Panel>
{changeLoggerActive && <ChangeLogger />}
{nodeInspectorActive && <NodeInspector />}
</div>
);
}
function DevToolButton({
active,
setActive,
children,
...rest
}: {
active: boolean;
setActive: Dispatch<SetStateAction<boolean>>;
children: ReactNode;
} & HTMLAttributes<HTMLButtonElement>) {
return (
<button onClick={() => setActive((a) => !a)} className={active ? 'active' : ''} {...rest}>
{children}
</button>
);
}
@@ -0,0 +1,63 @@
.react-flow__devtools {
--border-radius: 4px;
--highlight-color: rgba(238, 58, 115, 1);
--font: monospace, sans-serif;
border-radius: var(--border-radius);
font-size: 11px;
font-family: var(--font);
}
.react-flow__devtools button {
background: white;
border: none;
padding: 5px 15px;
color: #222;
font-weight: bold;
font-size: 12px;
cursor: pointer;
font-family: var(--font);
background-color: #f4f4f4;
}
.react-flow__devtools button:hover {
background: var(--highlight-color);
color: white;
}
.react-flow__devtools button.active {
background: var(--highlight-color);
color: white;
}
.react-flow__devtools button:first-child {
border-radius: var(--border-radius) 0 0 var(--border-radius);
border-right: 1px solid #ddd;
}
.react-flow__devtools button:last-child {
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.react-flow__devtools-changelogger {
pointer-events: none;
position: relative;
top: 50px;
left: 20px;
font-family: var(--font);
}
.react-flow__devtools-title {
font-weight: bold;
margin-bottom: 5px;
}
.react-flow__devtools-nodeinspector {
pointer-events: none;
font-family: monospace, sans-serif;
font-size: 10px;
}
.react-flow__devtools-nodeinfo {
top: 5px;
}
@@ -0,0 +1,55 @@
import { useCallback } from 'react';
import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react';
import DevTools from './DevTools';
const initNodes: Node[] = [
{
id: '1a',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{
id: '2a',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
{
id: '3a',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
},
{
id: '4a',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
},
];
const initEdges: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a' },
{ id: 'e1-3', source: '1a', target: '3a' },
];
const BasicFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
>
<DevTools />
</ReactFlow>
);
};
export default BasicFlow;
@@ -14,7 +14,7 @@ const initialNodes: Node[] = [
dragHandle: '.custom-drag-handle', dragHandle: '.custom-drag-handle',
style: { border: '1px solid #ddd', padding: '20px 40px' }, style: { border: '1px solid #ddd', padding: '20px 40px' },
position: { x: 200, y: 200 }, position: { x: 200, y: 200 },
data: null, data: {},
}, },
]; ];
@@ -33,6 +33,7 @@ const DragHandleFlow = () => {
edges={edges} edges={edges}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
nodeDragThreshold={0}
/> />
); );
}; };
@@ -39,7 +39,7 @@ const nodeOrigin: NodeOrigin = [0.5, 0.5];
const DnDFlow = () => { const DnDFlow = () => {
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>(); const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)); const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi); const onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi);
@@ -49,7 +49,7 @@ const DnDFlow = () => {
if (reactFlowInstance) { if (reactFlowInstance) {
const type = event.dataTransfer.getData('application/reactflow'); const type = event.dataTransfer.getData('application/reactflow');
const position = reactFlowInstance.screenToFlowCoordinate({ const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX, x: event.clientX,
y: event.clientY, y: event.clientY,
}); });
@@ -66,7 +66,7 @@ const DnDFlow = () => {
return ( return (
<div className={styles.dndflow}> <div className={styles.dndflow}>
<ReactFlowProvider nodes={initialNodes} edges={[]}> <ReactFlowProvider initialNodes={initialNodes} initialEdges={[]}>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -77,6 +77,7 @@ const DnDFlow = () => {
onInit={onInit} onInit={onInit}
onDrop={onDrop} onDrop={onDrop}
onDragOver={onDragOver} onDragOver={onDragOver}
nodeOrigin={nodeOrigin}
> >
<Controls /> <Controls />
</ReactFlow> </ReactFlow>
@@ -1,11 +1,11 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useStore, getStraightPath, EdgeProps } from '@xyflow/react'; import { useStore, getStraightPath, EdgeProps, useInternalNode } from '@xyflow/react';
import { getEdgeParams } from './utils.js'; import { getEdgeParams } from './utils.js';
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) { function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
const sourceNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === source), [source])); const sourceNode = useInternalNode(source);
const targetNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === target), [target])); const targetNode = useInternalNode(target);
if (!sourceNode || !targetNode) { if (!sourceNode || !targetNode) {
return null; return null;
@@ -5,7 +5,6 @@ import CustomNode from './CustomNode';
import FloatingEdge from './FloatingEdge'; import FloatingEdge from './FloatingEdge';
import CustomConnectionLine from './CustomConnectionLine'; import CustomConnectionLine from './CustomConnectionLine';
import '@xyflow/react/dist/style.css';
import './style.css'; import './style.css';
const initialNodes: Node[] = [ const initialNodes: Node[] = [
@@ -1,21 +1,19 @@
import { Node, Position, MarkerType, XYPosition } from '@xyflow/react'; import { Node, Position, MarkerType, XYPosition, InternalNode } from '@xyflow/react';
// this helper function returns the intersection point // this helper function returns the intersection point
// of the line between the center of the intersectionNode and the target node // of the line between the center of the intersectionNode and the target node
function getNodeIntersection(intersectionNode: Node, targetNode: Node) { function getNodeIntersection(intersectionNode: InternalNode, targetNode: InternalNode) {
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
const {
width: intersectionNodeWidth, const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured;
height: intersectionNodeHeight, const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
positionAbsolute: intersectionNodePosition, const targetPosition = targetNode.internals.positionAbsolute!;
} = intersectionNode;
const targetPosition = targetNode.positionAbsolute!;
const w = intersectionNodeWidth! / 2; const w = intersectionNodeWidth! / 2;
const h = intersectionNodeHeight! / 2; const h = intersectionNodeHeight! / 2;
const x2 = intersectionNodePosition!.x + w; const x2 = intersectionNodePosition.x + w;
const y2 = intersectionNodePosition!.y + h; const y2 = intersectionNodePosition.y + h;
const x1 = targetPosition.x + w; const x1 = targetPosition.x + w;
const y1 = targetPosition.y + h; const y1 = targetPosition.y + h;
@@ -31,8 +29,8 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
} }
// returns the position (top,right,bottom or right) passed node compared to the intersection point // returns the position (top,right,bottom or right) passed node compared to the intersection point
function getEdgePosition(node: Node, intersectionPoint: XYPosition) { function getEdgePosition(node: InternalNode, intersectionPoint: XYPosition) {
const n = { ...node.positionAbsolute, ...node }; const n = { ...node.internals.positionAbsolute, ...node };
const nx = Math.round(n.x!); const nx = Math.round(n.x!);
const ny = Math.round(n.y!); const ny = Math.round(n.y!);
const px = Math.round(intersectionPoint.x); const px = Math.round(intersectionPoint.x);
@@ -41,13 +39,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) { if (px <= nx + 1) {
return Position.Left; return Position.Left;
} }
if (px >= nx + n.width! - 1) { if (px >= nx + n.measured?.width! - 1) {
return Position.Right; return Position.Right;
} }
if (py <= ny + 1) { if (py <= ny + 1) {
return Position.Top; return Position.Top;
} }
if (py >= n.y! + n.height! - 1) { if (py >= n.y! + n.measured?.height! - 1) {
return Position.Bottom; return Position.Bottom;
} }
@@ -55,7 +53,7 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
} }
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge // returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
export function getEdgeParams(source: Node, target: Node) { export function getEdgeParams(source: InternalNode, target: InternalNode) {
const sourceIntersectionPoint = getNodeIntersection(source, target); const sourceIntersectionPoint = getNodeIntersection(source, target);
const targetIntersectionPoint = getNodeIntersection(target, source); const targetIntersectionPoint = getNodeIntersection(target, source);
@@ -175,6 +175,15 @@ const edgeTypes: EdgeTypes = {
custom2: CustomEdge2, custom2: CustomEdge2,
}; };
const defaultEdgeOptions = {
markerEnd: {
type: MarkerType.ArrowClosed,
color: 'red',
width: 20,
height: 20,
},
};
const EdgesFlow = () => { const EdgesFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -196,6 +205,8 @@ const EdgesFlow = () => {
onEdgeMouseEnter={onEdgeMouseEnter} onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave} onEdgeMouseLeave={onEdgeMouseLeave}
onDelete={console.log}
defaultEdgeOptions={defaultEdgeOptions}
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
+2 -2
View File
@@ -26,8 +26,8 @@ const buttonStyle: CSSProperties = {
}; };
const EmptyFlow = () => { const EmptyFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]); const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
const addRandomNode = () => { const addRandomNode = () => {
@@ -5,8 +5,8 @@ import { getEdgeParams } from './utils';
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => { const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
const { sourceNode, targetNode } = useStore((s) => { const { sourceNode, targetNode } = useStore((s) => {
const sourceNode = s.nodes.find((n) => n.id === source); const sourceNode = s.nodeLookup.get(source);
const targetNode = s.nodes.find((n) => n.id === target); const targetNode = s.nodeLookup.get(target);
return { sourceNode, targetNode }; return { sourceNode, targetNode };
}); });
@@ -1,15 +1,15 @@
import { Position, XYPosition, Node, Edge } from '@xyflow/react'; import { Position, XYPosition, Node, Edge, InternalNode } from '@xyflow/react';
// this helper function returns the intersection point // this helper function returns the intersection point
// of the line between the center of the intersectionNode and the target node // of the line between the center of the intersectionNode and the target node
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition { function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
const { const { position: intersectionNodePosition } = intersectionNode;
width: intersectionNodeWidth, const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured ?? {
height: intersectionNodeHeight, width: 0,
position: intersectionNodePosition, height: 0,
} = intersectionNode; };
const targetPosition = targetNode.position; const targetPosition = targetNode.position;
const w = (intersectionNodeWidth ?? 0) / 2; const w = (intersectionNodeWidth ?? 0) / 2;
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) { if (px <= nx + 1) {
return Position.Left; return Position.Left;
} }
if (px >= nx + (n.width ?? 0) - 1) { if (px >= nx + (n.measured?.width ?? 0) - 1) {
return Position.Right; return Position.Right;
} }
if (py <= ny + 1) { if (py <= ny + 1) {
return Position.Top; return Position.Top;
} }
if (py >= n.y + (n.height ?? 0) - 1) { if (py >= n.y + (n.measured?.height ?? 0) - 1) {
return Position.Bottom; return Position.Bottom;
} }
@@ -56,7 +56,7 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
} }
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge // returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
export function getEdgeParams(source: Node, target: Node) { export function getEdgeParams(source: InternalNode, target: InternalNode) {
const sourceIntersectionPoint = getNodeIntersection(source, target); const sourceIntersectionPoint = getNodeIntersection(source, target);
const targetIntersectionPoint = getNodeIntersection(target, source); const targetIntersectionPoint = getNodeIntersection(target, source);
+8 -2
View File
@@ -11,6 +11,7 @@ import {
MiniMap, MiniMap,
Controls, Controls,
} from '@xyflow/react'; } from '@xyflow/react';
import ReactFlowDevTools from '../DevTools/DevTools';
const initialNodes: Node[] = [ const initialNodes: Node[] = [
{ {
@@ -48,8 +49,10 @@ const initialEdges: Edge[] = [
const setHidden = (hidden: boolean) => (els: any[]) => const setHidden = (hidden: boolean) => (els: any[]) =>
els.map((e: any) => { els.map((e: any) => {
e.hidden = hidden; return {
return e; ...e,
hidden,
};
}); });
const HiddenFlow = () => { const HiddenFlow = () => {
@@ -70,6 +73,8 @@ const HiddenFlow = () => {
setEdges(setHidden(isHidden)); setEdges(setHidden(isHidden));
}, [isHidden, setEdges, setNodes]); }, [isHidden, setEdges, setNodes]);
console.log(nodes);
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -80,6 +85,7 @@ const HiddenFlow = () => {
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
<ReactFlowDevTools position="top-right" />
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}> <div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
<div> <div>
@@ -36,7 +36,7 @@ const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', nod
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge); const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event); const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event);
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event); const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('onPaneContextMenu', event); const onPaneContextMenu = (event: ReactMouseEvent | MouseEvent) => console.log('onPaneContextMenu', event);
const onMoveEnd = (_: TouchEvent | MouseEvent | null, viewport: Viewport) => console.log('onMoveEnd', viewport); const onMoveEnd = (_: TouchEvent | MouseEvent | null, viewport: Viewport) => console.log('onMoveEnd', viewport);
const InteractionFlow = () => { const InteractionFlow = () => {
@@ -80,6 +80,7 @@ const InteractionFlow = () => {
onPaneClick={captureZoomClick ? onPaneClick : undefined} onPaneClick={captureZoomClick ? onPaneClick : undefined}
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined} onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined} onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
nodeDragThreshold={0}
onMoveEnd={onMoveEnd} onMoveEnd={onMoveEnd}
> >
<MiniMap /> <MiniMap />
@@ -17,28 +17,34 @@ const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', n
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initialNodes: Node[] = [ const initialNodes: Node[] = [
{
id: '0',
data: { label: 'rectangle' },
position: { x: 0, y: 0 },
width: 100,
height: 100,
draggable: false,
style: {
opacity: 0.5,
},
},
{ {
id: '1', id: '1',
type: 'input', type: 'input',
data: { label: 'Node 1' }, data: { label: 'Node 1' },
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
className: 'light', width: 200,
style: { height: 100,
width: 200,
height: 100,
},
}, },
{ {
id: '2', id: '2',
data: { label: 'Node 2' }, data: { label: 'Node 2' },
position: { x: 0, y: 150 }, position: { x: 0, y: 150 },
className: 'light',
}, },
{ {
id: '3', id: '3',
data: { label: 'Node 3' }, data: { label: 'Node 3' },
position: { x: 250, y: 0 }, position: { x: 250, y: 0 },
className: 'light',
}, },
{ {
id: '4', id: '4',
@@ -48,7 +54,6 @@ const initialNodes: Node[] = [
width: 50, width: 50,
height: 50, height: 50,
}, },
className: 'light',
}, },
]; ];
@@ -19,6 +19,7 @@ import {
import initialItems from './initial-elements'; import initialItems from './initial-elements';
import styles from './layouting.module.css'; import styles from './layouting.module.css';
import ReactFlowDevTools from '../DevTools/DevTools';
const dagreGraph = new dagre.graphlib.Graph(); const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({})); dagreGraph.setDefaultEdgeLabel(() => ({}));
@@ -56,16 +57,16 @@ const LayoutFlow = () => {
const layoutedNodes = nodes.map((node) => { const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id); const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// we need to pass a slightly different position in order to notify react flow about the change
// @TODO how can we change the position handling so that we dont need this hack?
node.position = {
x: nodeWithPosition.x + Math.random() / 1000,
y: nodeWithPosition.y,
};
return node; return {
...node,
targetPosition: isHorizontal ? Position.Left : Position.Top,
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
position: {
x: nodeWithPosition.x,
y: nodeWithPosition.y,
},
};
}); });
setNodes(layoutedNodes); setNodes(layoutedNodes);
@@ -98,6 +99,7 @@ const LayoutFlow = () => {
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
> >
<Controls /> <Controls />
<ReactFlowDevTools />
</ReactFlow> </ReactFlow>
<Panel position="top-right"> <Panel position="top-right">
<button onClick={() => onLayout('TB')}>vertical layout</button> <button onClick={() => onLayout('TB')}>vertical layout</button>
@@ -0,0 +1,92 @@
import { useCallback } from 'react';
import {
ReactFlow,
Controls,
addEdge,
Connection,
useNodesState,
useEdgesState,
Background,
Node,
Edge,
Panel,
useReactFlow,
ReactFlowProvider,
} from '@xyflow/react';
import './style.css';
const initNodes: Node[] = [];
for (let i = 0; i < 100; i++) {
initNodes.push({
id: i.toString(),
data: {
label: `node ${i + 1}`,
},
position: { x: (i % 10) * 60, y: Math.floor(i / 10) * 60 },
});
}
const initEdges: Edge[] = initNodes.reduce<Edge[]>((res, node, index) => {
if (index > 0) {
res.push({ id: `${index - 1}-${index}`, source: (index - 1).toString(), target: index.toString() });
}
return res;
}, []);
const CustomNodeFlow = () => {
const { setNodes, updateNodeData, updateEdge } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
const multiSetNodes = () => {
nodes.forEach((node) =>
setNodes((nds) =>
nds.map((n) => {
if (n.id === node.id) {
return { ...n, data: { label: 'node set' } };
}
return n;
})
)
);
};
const multiUpdateNodes = () => {
nodes.forEach((node) => updateNodeData(node.id, { label: 'node update' }));
};
const multiUpdateEdges = () => {
edges.forEach((edge) => updateEdge(edge.id, { label: 'edge update' }));
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
className="multiset"
>
<Controls />
<Background />
<Panel>
<button onClick={multiSetNodes}>set nodes</button>
<button onClick={multiUpdateNodes}>update nodes</button>
<button onClick={multiUpdateEdges}>update edges</button>
</Panel>
</ReactFlow>
);
};
export default () => (
<ReactFlowProvider>
<CustomNodeFlow />
</ReactFlowProvider>
);
@@ -0,0 +1,3 @@
.multiset .react-flow__node {
width: 50px;
}
@@ -1,187 +0,0 @@
import { useState, MouseEvent, useCallback } from 'react';
import {
ReactFlow,
Controls,
MiniMap,
Background,
addEdge,
useNodesState,
useEdgesState,
Node,
Edge,
ReactFlowInstance,
Connection,
} from '@xyflow/react';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 200, height: 200 },
},
{
id: '2a',
data: { label: 'Node 2a' },
position: { x: 10, y: 50 },
parentNode: '2',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 320, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 320, y: 200 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: 300, height: 300 },
},
{
id: '4a',
data: { label: 'Node 4a' },
position: { x: 15, y: 65 },
className: 'light',
parentNode: '4',
extent: 'parent',
},
{
id: '4b',
data: { label: 'Node 4b' },
position: { x: 15, y: 120 },
className: 'light',
style: {
backgroundColor: 'rgba(255, 0, 255, 0.7)',
height: 150,
width: 270,
},
parentNode: '4',
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 20, y: 40 },
className: 'light',
parentNode: '4b',
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 100, y: 100 },
className: 'light',
parentNode: '4b',
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2a-4a', source: '2a', target: '4a' },
{ id: 'e3-4', source: '3', target: '4' },
{ id: 'e3-4b', source: '3', target: '4b' },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
];
const NestedFlow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
},
[setEdges]
);
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
const updatePos = () => {
setNodes((nds) => {
return nds.map((n) => {
n.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return n;
});
});
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light';
return n;
});
});
};
const toggleChildNodes = () => {
setNodes((nds) => {
return nds.map((n) => {
n.hidden = !!n.parentNode && !n.hidden;
return n;
});
});
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false}
>
<MiniMap pannable />
<Controls />
<Background />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
toggleChildNodes
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default NestedFlow;
@@ -9,7 +9,7 @@ const DefaultResizerNode: FC<NodeProps> = ({ data, selected }) => {
maxWidth={data.maxWidth ?? undefined} maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined} minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined} maxHeight={data.maxHeight ?? undefined}
isVisible={data.isVisible ?? selected} isVisible={data.isVisible ?? !!selected}
shouldResize={data.shouldResize ?? undefined} shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined} onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined} onResize={data.onResize ?? undefined}
@@ -16,8 +16,6 @@ import CustomResizer from './CustomResizer';
import VerticalResizer from './VerticalResizer'; import VerticalResizer from './VerticalResizer';
import HorizontalResizer from './HorizontalResizer'; import HorizontalResizer from './HorizontalResizer';
import '@xyflow/react/dist/style.css';
const nodeTypes = { const nodeTypes = {
defaultResizer: DefaultResizer, defaultResizer: DefaultResizer,
customResizer: CustomResizer, customResizer: CustomResizer,
@@ -39,6 +37,7 @@ const initialNodes: Node[] = [
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'default resizer' }, data: { label: 'default resizer' },
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
origin: [1, 1],
style: { ...nodeStyle }, style: { ...nodeStyle },
}, },
{ {
@@ -52,7 +51,9 @@ const initialNodes: Node[] = [
maxHeight: 200, maxHeight: 200,
}, },
position: { x: 0, y: 60 }, position: { x: 0, y: 60 },
style: { ...nodeStyle, width: 100, height: 80 }, width: 100,
height: 80,
style: { ...nodeStyle },
}, },
{ {
id: '1b', id: '1b',
@@ -66,9 +67,9 @@ const initialNodes: Node[] = [
maxHeight: 400, maxHeight: 400,
}, },
position: { x: 250, y: 0 }, position: { x: 250, y: 0 },
width: 174,
height: 123,
style: { style: {
width: 174,
height: 123,
...nodeStyle, ...nodeStyle,
}, },
}, },
@@ -77,7 +78,9 @@ const initialNodes: Node[] = [
type: 'customResizer', type: 'customResizer',
data: { label: 'custom resize icon' }, data: { label: 'custom resize icon' },
position: { x: 0, y: 200 }, position: { x: 0, y: 200 },
style: { width: 100, height: 60, ...nodeStyle }, width: 100,
height: 60,
style: { ...nodeStyle },
}, },
{ {
id: '3', id: '3',
@@ -96,7 +99,8 @@ const initialNodes: Node[] = [
keepAspectRatio: true, keepAspectRatio: true,
}, },
position: { x: 400, y: 200 }, position: { x: 400, y: 200 },
style: { ...nodeStyle, height: 50 }, height: 50,
style: { ...nodeStyle },
}, },
{ {
id: '4', id: '4',
@@ -118,6 +122,50 @@ const initialNodes: Node[] = [
position: { x: 250, y: 400 }, position: { x: 250, y: 400 },
style: { ...nodeStyle }, style: { ...nodeStyle },
}, },
{
id: '5',
type: 'defaultResizer',
data: { label: 'Parent', keepAspectRatio: true },
position: { x: 700, y: 0 },
width: 300,
height: 300,
style: { ...nodeStyle },
},
{
id: '5a',
type: 'defaultResizer',
data: {
label: 'Child with extent: parent',
},
position: { x: 50, y: 50 },
parentId: '5',
extent: 'parent',
width: 50,
height: 100,
style: { ...nodeStyle },
},
{
id: '5b',
type: 'defaultResizer',
data: { label: 'Child with expandParent' },
position: { x: 100, y: 100 },
width: 100,
height: 100,
parentId: '5',
expandParent: true,
style: { ...nodeStyle },
},
{
id: '5c',
type: 'defaultResizer',
data: { label: 'Child with expandParent & keepAspectRatio' },
position: { x: 250, y: 200 },
height: 100,
width: 100,
parentId: '5',
expandParent: true,
style: { ...nodeStyle },
},
]; ];
const CustomNodeFlow = () => { const CustomNodeFlow = () => {
@@ -142,6 +190,7 @@ const CustomNodeFlow = () => {
maxZoom={5} maxZoom={5}
snapToGrid={snapToGrid} snapToGrid={snapToGrid}
fitView fitView
onlyRenderVisibleElements
> >
<Controls /> <Controls />
<Panel position="bottom-right"> <Panel position="bottom-right">
@@ -28,6 +28,14 @@ const initialNodes: Node[] = [
data: { label: 'A Node' }, data: { label: 'A Node' },
position: { x: 250, y: 0 }, position: { x: 250, y: 0 },
}, },
{
id: '3',
type: 'b',
sourcePosition: Position.Right,
targetPosition: Position.Left,
data: { label: 'B Node' },
position: { x: 350, y: 0 },
},
]; ];
const buttonStyle: CSSProperties = { const buttonStyle: CSSProperties = {
@@ -66,7 +74,7 @@ const nodeTypesObjects: NodeTypesObject = {
const NodeTypeChangeFlow = () => { const NodeTypeChangeFlow = () => {
const [nodeTypesId, setNodeTypesId] = useState<string>('a'); const [nodeTypesId, setNodeTypesId] = useState<string>('a');
const [nodes, , onNodesChange] = useNodesState(initialNodes); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]); const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a')); const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
@@ -78,6 +86,7 @@ const NodeTypeChangeFlow = () => {
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
nodeTypes={nodeTypesObjects[nodeTypesId]} nodeTypes={nodeTypesObjects[nodeTypesId]}
fitView
> >
<button onClick={changeType} style={buttonStyle}> <button onClick={changeType} style={buttonStyle}>
change type change type
+14 -8
View File
@@ -15,6 +15,8 @@ import {
Background, Background,
MiniMap, MiniMap,
ConnectionMode, ConnectionMode,
OnBeforeDelete,
OnDelete,
} from '@xyflow/react'; } from '@xyflow/react';
const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes); const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
@@ -23,7 +25,7 @@ const onNodeDragStop = (_: ReactMouseEvent, node: Node, nodes: Node[]) => consol
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node); const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event); const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event); const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('pane context menu', event); const onPaneContextMenu = (event: ReactMouseEvent | MouseEvent) => console.log('pane context menu', event);
const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes); const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes); const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes); const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
@@ -45,8 +47,12 @@ const onEdgeMouseEnter = (_: ReactMouseEvent, edge: Edge) => console.log('edge m
const onEdgeMouseMove = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse move', edge); const onEdgeMouseMove = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse move', edge);
const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge); const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge); const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge);
const onNodesDelete = (nodes: Node[]) => console.log('nodes delete', nodes); const onBeforeDelete: OnBeforeDelete = async ({ nodes, edges }) => {
const onEdgesDelete = (edges: Edge[]) => console.log('edges delete', edges); console.log('on before delete', nodes, edges);
const deleteElements = confirm('Do you want to remove the selected elements?');
return deleteElements;
};
const onDelete: OnDelete = ({ nodes, edges }) => console.log('on delete', nodes, edges);
const onPaneMouseMove = (e: ReactMouseEvent) => console.log('pane move', e.clientX, e.clientY); const onPaneMouseMove = (e: ReactMouseEvent) => console.log('pane move', e.clientX, e.clientY);
const initialNodes: Node[] = [ const initialNodes: Node[] = [
@@ -226,13 +232,13 @@ const OverviewFlow = () => {
fitViewOptions={{ padding: 0.1 /*nodes: [{ id: '1' }]*/ }} fitViewOptions={{ padding: 0.1 /*nodes: [{ id: '1' }]*/ }}
attributionPosition="top-right" attributionPosition="top-right"
maxZoom={Infinity} maxZoom={Infinity}
onNodesDelete={onNodesDelete} onBeforeDelete={onBeforeDelete}
onEdgesDelete={onEdgesDelete} onDelete={onDelete}
onPaneMouseMove={onPaneMouseMove} onPaneMouseMove={onPaneMouseMove}
> >
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} /> <MiniMap nodeBorderRadius={2} />
<Controls /> <Controls orientation="horizontal" />
<Background color="#aaa" gap={25} /> <Background gap={25} />
</ReactFlow> </ReactFlow>
); );
}; };
@@ -2,11 +2,10 @@ import { useState, useCallback, MouseEvent as ReactMouseEvent } from 'react';
import { import {
ReactFlow, ReactFlow,
Controls, Controls,
updateEdge, reconnectEdge,
addEdge, addEdge,
applyNodeChanges, applyNodeChanges,
applyEdgeChanges, applyEdgeChanges,
ReactFlowInstance,
Connection, Connection,
Edge, Edge,
Node, Node,
@@ -92,26 +91,24 @@ const initialNodes: Node[] = [
]; ];
const initialEdges: Edge[] = [ const initialEdges: Edge[] = [
{ id: 'e1-3', source: '1', target: '3', label: 'This edge can only be updated from source', updatable: 'source' }, { id: 'e1-3', source: '1', target: '3', label: 'This edge can only be updated from source', reconnectable: 'source' },
{ id: 'e2-4', source: '2', target: '4', label: 'This edge can only be updated from target', updatable: 'target' }, { id: 'e2-4', source: '2', target: '4', label: 'This edge can only be updated from target', reconnectable: 'target' },
{ id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' }, { id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' },
]; ];
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView(); const onReconnectStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
console.log(`start update ${handleType} handle`, edge); console.log(`start update ${handleType} handle`, edge);
const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => const onReconnectEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) =>
console.log(`end update ${handleType} handle`, edge); console.log(`end update ${handleType} handle`, edge);
const UpdatableEdge = () => { const ReconnectEdge = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes); const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => const onReconnect = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els)); setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els)); const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
const onNodesChange = useCallback((changes: NodeChange[]) => { const onNodesChange = useCallback((changes: NodeChange[]) => {
console.log(changes);
setNodes((ns) => applyNodeChanges(changes, ns)); setNodes((ns) => applyNodeChanges(changes, ns));
}, []); }, []);
@@ -125,16 +122,16 @@ const UpdatableEdge = () => {
edges={edges} edges={edges}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onInit={onInit}
snapToGrid={true} snapToGrid={true}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onConnect={onConnect} onConnect={onConnect}
onEdgeUpdateStart={onEdgeUpdateStart} onReconnectStart={onReconnectStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onReconnectEnd={onReconnectEnd}
fitView
> >
<Controls /> <Controls />
</ReactFlow> </ReactFlow>
); );
}; };
export default UpdatableEdge; export default ReconnectEdge;
@@ -0,0 +1,29 @@
import { ReactFlow } from '@xyflow/react';
import { useDispatch, useSelector, Provider } from 'react-redux';
import { onNodesChange, onEdgesChange, setSelectedNodesAndEdges, store } from './state';
const OverviewFlow = () => {
const dispatch = useDispatch();
const nodes = useSelector((state) => state.myApplication.nodes);
const edges = useSelector((state) => state.myApplication.edges);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={(e) => dispatch(onNodesChange(e))}
onEdgesChange={(e) => dispatch(onEdgesChange(e))}
onSelectionChange={(e) => dispatch(setSelectedNodesAndEdges(e))}
fitView
attributionPosition="top-right"
/>
);
};
export default () => (
<Provider store={store}>
<OverviewFlow />
</Provider>
);
@@ -0,0 +1,98 @@
import { MarkerType, type Node, type Edge } from '@xyflow/react';
export const nodes: Node[] = [
{
id: '1',
type: 'input',
data: {
label: 'hey',
},
position: { x: 250, y: 0 },
},
{
id: '2',
data: {
label: 'default node',
},
position: { x: 100, y: 100 },
},
{
id: '3',
data: {
label: 'custom style',
},
position: { x: 400, y: 100 },
style: {
background: '#D6D5E6',
color: '#333',
border: '1px solid #222138',
width: 180,
},
},
{
id: '4',
position: { x: 250, y: 200 },
data: {
label: 'Another default node',
},
},
{
id: '5',
data: {
label: 'Node id: 5',
},
position: { x: 250, y: 325 },
},
{
id: '6',
type: 'output',
data: {
label: 'output',
},
position: { x: 100, y: 480 },
},
{
id: '7',
type: 'output',
data: { label: 'Another output node' },
position: { x: 400, y: 450 },
},
];
export const edges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
{ id: 'e1-3', source: '1', target: '3' },
{
id: 'e3-4',
source: '3',
target: '4',
animated: true,
label: 'animated edge',
},
{
id: 'e4-5',
source: '4',
target: '5',
label: 'edge with arrow head',
markerEnd: {
type: MarkerType.ArrowClosed,
},
},
{
id: 'e5-6',
source: '5',
target: '6',
type: 'smoothstep',
label: 'smooth step edge',
},
{
id: 'e5-7',
source: '5',
target: '7',
type: 'step',
style: { stroke: '#f6ab6c' },
label: 'a step edge',
animated: true,
labelStyle: { fill: '#f6ab6c', fontWeight: 700 },
},
];
@@ -0,0 +1,59 @@
import { createSlice, configureStore } from '@reduxjs/toolkit';
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
import { nodes, edges } from './initial-elements';
const initialState = {
nodes,
edges,
selectedNodes: [],
selectedEdges: [],
};
const setNodesReducer = (state, action) => {
state.nodes = action.payload;
};
const setEdgesReducer = (state, action) => {
state.edges = action.payload;
};
const onNodesChangeReducer = (state, action) => {
const a = applyNodeChanges(action.payload, state.nodes);
state.nodes = a;
};
const onEdgesChangeReducer = (state, action) => {
const a = applyEdgeChanges(action.payload, state.edges);
state.edges = a;
};
const setSelectedNodesAndEdgesReducer = (state, action) => {
state.selectedNodes = action.payload.nodes;
state.selectedEdges = action.payload.edges;
};
const setSelectedNodesReducer = (state, action) => {
state.selectedNodes = action.payload;
};
const MyApplicationSlice = createSlice({
name: 'MyApplication',
initialState,
reducers: {
setNodes: setNodesReducer,
setEdges: setEdgesReducer,
onNodesChange: onNodesChangeReducer,
onEdgesChange: onEdgesChangeReducer,
setSelectedNodesAndEdges: setSelectedNodesAndEdgesReducer,
setSelectedNodes: setSelectedNodesReducer,
},
});
export const { setNodes, setEdges, onNodesChange, onEdgesChange, setSelectedNodesAndEdges, setSelectedNodes } =
MyApplicationSlice.actions;
export const store = configureStore({
reducer: {
myApplication: MyApplicationSlice.reducer,
},
});
@@ -0,0 +1,70 @@
import { useCallback } from 'react';
import {
ReactFlow,
MiniMap,
Background,
BackgroundVariant,
Controls,
ReactFlowProvider,
Node,
Edge,
useReactFlow,
Panel,
} from '@xyflow/react';
const a = { id: 'a', data: { label: 'A' }, position: { x: 250, y: 5 } };
const b = { id: 'b', data: { label: 'B' }, position: { x: 100, y: 100 } };
const c = { id: 'c', data: { label: 'C' }, position: { x: 400, y: 100 } };
const SetNotesBatchingFlow = () => {
const { setNodes, updateNode } = useReactFlow();
const triggerMultipleSetNodes = useCallback(() => {
setNodes([a]);
setNodes((nodes) => [...nodes, b]);
setNodes((nodes) => [...nodes, c]);
setNodes((nodes) =>
nodes.map((node) =>
node.id === 'a' ? { ...node, position: { x: node.position.x + 20, y: node.position.y + 20 } } : node
)
);
}, []);
const triggerMultipleUpdateNodes = useCallback(() => {
triggerMultipleSetNodes();
updateNode('a', (a) => ({ position: { x: a.position.x + 20, y: a.position.y + 20 } }));
updateNode('b', (b) => ({ position: { x: b.position.x + 20, y: b.position.y + 20 } }));
updateNode('c', (c) => ({ position: { x: c.position.x + 20, y: c.position.y + 20 } }));
updateNode('a', (a) => ({ data: { ...a.data, label: `A ${Date.now()}` } }));
updateNode('b', (b) => ({ data: { ...b.data, label: `B ${Date.now()}` } }));
updateNode('c', (c) => ({ data: { ...c.data, label: `C ${Date.now()}` } }));
}, []);
return (
<ReactFlow
defaultNodes={[]}
defaultEdges={[]}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
fitView
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
<Panel position="top-right">
<button onClick={triggerMultipleSetNodes}>queue multiple setNodes calls</button>
<button onClick={triggerMultipleUpdateNodes}>queue multiple updateNode calls</button>
</Panel>
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<SetNotesBatchingFlow />
</ReactFlowProvider>
);
}
+168 -10
View File
@@ -1,7 +1,6 @@
import { useState, useCallback } from 'react'; import { useState, useCallback, useRef, useEffect } from 'react';
import { import {
ReactFlow, ReactFlow,
ReactFlowInstance,
Edge, Edge,
Node, Node,
NodeChange, NodeChange,
@@ -12,16 +11,11 @@ import {
EdgeChange, EdgeChange,
Controls, Controls,
Background, Background,
MiniMap,
Panel, Panel,
} from '@xyflow/react'; } from '@xyflow/react';
import { getNodesAndEdges } from './utils'; import { getNodesAndEdges } from './utils';
import { FrameRecorder, generateMouseEventParamsTargetingNode, nextFrame } from './performanceUtils';
const onInit = (reactFlowInstance: ReactFlowInstance) => {
reactFlowInstance.fitView();
console.log(reactFlowInstance.getNodes());
};
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25); const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
@@ -31,6 +25,166 @@ const StressFlow = () => {
const onConnect = useCallback((connection: Connection) => { const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds)); setEdges((eds) => addEdge(connection, eds));
}, []); }, []);
const dragInViewport = async () => {
// Note: selecting specifically node 18, as its normally located in the right part of the viewport
// which means dragging it left is safe to do without scrolling the viewport.
const nodeElement = document.querySelector('.react-flow__node[data-id="18"]');
if (!nodeElement) throw new Error('Node with id 18 not found');
const frameRecorder = new FrameRecorder();
// Hold down the mouse
frameRecorder.setStage('mousedown');
const mouseDownEvent = generateMouseEventParamsTargetingNode(nodeElement);
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseDownEvent));
await nextFrame();
// Start at the node position and move the mouse 5px to the left on every frame
frameRecorder.setStage('mousemove');
let currentXPosition = mouseDownEvent.clientX;
for (let iteration = 0; iteration < 20; ++iteration) {
const movementX = -5;
currentXPosition += movementX;
nodeElement.dispatchEvent(
new MouseEvent('mousemove', {
...mouseDownEvent,
clientX: currentXPosition,
screenX: currentXPosition,
movementX,
})
);
await nextFrame();
}
// Release the mouse
frameRecorder.setStage('mouseup');
nodeElement.dispatchEvent(
new MouseEvent('mouseup', {
...mouseDownEvent,
clientX: currentXPosition,
screenX: currentXPosition,
})
);
await nextFrame();
// Log the results
await frameRecorder.endRecordingAsync();
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
};
const dragOutsideViewport = async () => {
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodes[randomNodeIndex].id}"]`);
if (!nodeElement) throw new Error('Node not found');
const frameRecorder = new FrameRecorder();
// Hold down the mouse
frameRecorder.setStage('mousedown');
const mouseDownEvent = generateMouseEventParamsTargetingNode(nodeElement);
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseDownEvent));
await nextFrame();
// Move the mouse to the top of the viewport (so that the viewport starts
// scrolling up). Then, wiggle the mouse up and down to keep the viewport
// scrolling.
frameRecorder.setStage('mousemove');
let currentYPosition = 50;
for (let iteration = 0; iteration < 20; ++iteration) {
const movementY = Math.random() > 0.5 ? +2 : -2;
currentYPosition += movementY;
nodeElement.dispatchEvent(
new MouseEvent('mousemove', {
...mouseDownEvent,
clientY: currentYPosition,
screenY: currentYPosition,
movementY,
})
);
await nextFrame();
}
// Release the mouse
frameRecorder.setStage('mouseup');
nodeElement.dispatchEvent(
new MouseEvent('mouseup', {
...mouseDownEvent,
clientY: currentYPosition,
screenY: currentYPosition,
})
);
await nextFrame();
// Log the results
await frameRecorder.endRecordingAsync();
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
};
const selectNode = async () => {
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodes[randomNodeIndex].id}"]`);
if (!nodeElement) throw new Error('Node not found');
const frameRecorder = new FrameRecorder();
const mouseEvent = generateMouseEventParamsTargetingNode(nodeElement);
// mousedown
frameRecorder.setStage('mousedown');
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseEvent));
await nextFrame();
// click
frameRecorder.setStage('click');
nodeElement.dispatchEvent(new MouseEvent('click', mouseEvent));
await nextFrame();
// mouseup
frameRecorder.setStage('mouseup');
nodeElement.dispatchEvent(new MouseEvent('mouseup', mouseEvent));
await nextFrame();
// Log the results
await frameRecorder.endRecordingAsync();
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
};
const [key, setKey] = useState(0);
const frameRecorderRef = useRef<FrameRecorder | null>(null);
function remount() {
frameRecorderRef.current = new FrameRecorder();
setKey((k) => k + 1);
}
useEffect(() => {
const frameRecorder = frameRecorderRef.current;
if (!frameRecorder) return;
frameRecorder.endRecordingAsync().then(() => {
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
frameRecorderRef.current = null;
});
}, [key]);
const updatePos = () => { const updatePos = () => {
setNodes((nds) => { setNodes((nds) => {
return nds.map((n) => { return nds.map((n) => {
@@ -62,19 +216,23 @@ const StressFlow = () => {
return ( return (
<ReactFlow <ReactFlow
key={key}
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
onInit={onInit}
onConnect={onConnect} onConnect={onConnect}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgeChange} onEdgesChange={onEdgeChange}
minZoom={0.2} minZoom={0.2}
fitView
> >
<MiniMap />
<Controls /> <Controls />
<Background /> <Background />
<Panel position="top-right"> <Panel position="top-right">
<button onClick={selectNode}>select node</button>
<button onClick={dragInViewport}>drag node within the viewport</button>
<button onClick={dragOutsideViewport}>drag node outside of the viewport</button>
<button onClick={remount}>re-mount</button>
<button onClick={updatePos}>change pos</button> <button onClick={updatePos}>change pos</button>
<button onClick={updateElements}>update elements</button> <button onClick={updateElements}>update elements</button>
</Panel> </Panel>
@@ -0,0 +1,150 @@
type Frame = {
duration: number;
stage: string;
};
/**
* Measures and outputs the duration of every frame that happens between the
* instance is created and `endRecording()` is called.
*
* Usage:
*
* ```ts
* const recorder = new FrameRecorder();
*
* // Do some performance-intensive stuff
*
* await recorder.endRecordingAsync();
*
* console.log(recorder.getFrames());
* console.log(recorder.getFramesForObservable()); // → paste into https://observablehq.com/@iamakulov/long-frame-visualizer
* ```
*/
export class FrameRecorder {
private frames: Frame[] = [];
private animationFrameId: number;
private stage: string = '<no stage>';
constructor() {
let lastFrameTimestamp = performance.now();
const measureFrame = () => {
const timestamp = performance.now();
// Visualize the frames in the Performance pane (see the collapsed
// “Timings” section) so its easier to see what exactly each frame
// captured
performance.measure(`frame (${this.stage})`, {
start: lastFrameTimestamp,
end: timestamp,
});
this.frames.push({
duration: timestamp - lastFrameTimestamp,
stage: this.stage,
});
lastFrameTimestamp = timestamp;
this.animationFrameId = requestAnimationFrame(measureFrame);
};
this.animationFrameId = requestAnimationFrame(measureFrame);
}
// The method is explicitly marked `async` in its name to make sure the caller
// doesnt forget to `await` it. (Otherwise, some events might be lost.)
async endRecordingAsync() {
this.setStage('waiting for idle');
await new Promise((resolve) => requestIdleCallback(resolve));
requestAnimationFrame(() => {
cancelAnimationFrame(this.animationFrameId);
});
}
/**
* Adds an optional annotation to all subsequent frames. Useful to
* differentiate frames from different events e.g. you can call
* `setState("mousedown")` before dispatching a mousedown event, and then
* `setState("mouseup")` before a mouseup one.
*
* When used, will affect both `getFramesForObservable()` and `getFrames()`.
*/
setStage(stage: string) {
this.stage = stage;
}
getFramesForObservable() {
return this.frames.map((frame, index) => ({ ...frame, index }));
}
getFrames() {
// Group frames by stage so you could see which frames originated from `mousedown` vs `mousemove` vs `mouseup` events
const framesPerStage: Record<string, number[]> = {};
for (const frame of this.frames) {
const stage = frame.stage;
if (!framesPerStage[stage]) {
framesPerStage[stage] = [];
}
framesPerStage[stage].push(frame.duration);
}
// If theres only one stage, return the frames directly
return framesPerStage;
}
}
/**
* Returns a promise that resolves when the next frame starts, and everything
* that was already scheduled in the event queue has been processed.
*/
export function nextFrame() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
/**
* Generates params for a new MouseEvent() that will target the given node.
*/
export function generateMouseEventParamsTargetingNode(node: Element) {
const nodePosition = node.getBoundingClientRect();
// Lets make the event (eg click) happen 5px to the right and 5px to the
// bottom of the nodes top-left corner
const positionRelativeToNode = {
left: 5,
top: 5,
};
return {
clientX: Math.round(nodePosition.left + positionRelativeToNode.left),
clientY: Math.round(nodePosition.top + positionRelativeToNode.top),
movementX: 0,
movementY: 0,
offsetX: positionRelativeToNode.left,
offsetY: positionRelativeToNode.top,
screenX: Math.round(nodePosition.left + positionRelativeToNode.left),
screenY: Math.round(nodePosition.top + positionRelativeToNode.top),
// Required boilerplate
altKey: false,
bubbles: true,
button: 0,
buttons: 1,
cancelBubble: false,
cancelable: true,
composed: true,
ctrlKey: false,
currentTarget: null,
defaultPrevented: false,
detail: 1,
eventPhase: 0,
fromElement: null,
isTrusted: true,
metaKey: false,
relatedTarget: null,
returnValue: true,
shiftKey: false,
view: window,
which: 1,
};
}
@@ -20,8 +20,6 @@ export function getNodesAndEdges(xElements = 10, yElements = 10): ElementsCollec
style: { width: 50, height: 30, fontSize: 11 }, style: { width: 50, height: 30, fontSize: 11 },
data, data,
position, position,
width: 50,
height: 30,
}; };
initialNodes.push(node); initialNodes.push(node);
@@ -1,6 +1,6 @@
import React, { memo, FC, CSSProperties } from 'react'; import React, { memo, FC, CSSProperties } from 'react';
import { Handle, NodeProps, Position } from '@xyflow/react'; import { Handle, NodeProps, Position, useInternalNode } from '@xyflow/react';
const infoStyle: CSSProperties = { fontSize: 11 }; const infoStyle: CSSProperties = { fontSize: 11 };
const idStyle: CSSProperties = { const idStyle: CSSProperties = {
@@ -11,13 +11,18 @@ const idStyle: CSSProperties = {
left: 2, left: 2,
}; };
const DebugNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => { const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => {
const node = useInternalNode(id)!;
return ( return (
<> <>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
<div style={idStyle}>{id}</div> <div style={idStyle}>{id}</div>
<div style={infoStyle}> <div style={infoStyle}>
x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex} x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex}
</div>
<div style={infoStyle}>
x:{Math.round(node.position.x)} y:{Math.round(node.position.y)}
</div> </div>
<Handle type="source" position={Position.Bottom} /> <Handle type="source" position={Position.Bottom} />
</> </>
+40 -29
View File
@@ -13,6 +13,9 @@ import {
MiniMap, MiniMap,
Background, Background,
Panel, Panel,
NodeOrigin,
useUpdateNodeInternals,
ReactFlowProvider,
} from '@xyflow/react'; } from '@xyflow/react';
import DebugNode from './DebugNode'; import DebugNode from './DebugNode';
@@ -49,7 +52,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 4a' }, data: { label: 'Node 4a' },
position: { x: 15, y: 15 }, position: { x: 15, y: 15 },
className: 'light', className: 'light',
parentNode: '4', parentId: '4',
origin: [0.5, 0.5], origin: [0.5, 0.5],
extent: [ extent: [
@@ -67,21 +70,21 @@ const initialNodes: Node[] = [
height: 200, height: 200,
width: 300, width: 300,
}, },
parentNode: '4', parentId: '4',
}, },
{ {
id: '4b1', id: '4b1',
data: { label: 'Node 4b1' }, data: { label: 'Node 4b1' },
position: { x: 40, y: 20 }, position: { x: 40, y: 20 },
className: 'light', className: 'light',
parentNode: '4b', parentId: '4b',
}, },
{ {
id: '4b2', id: '4b2',
data: { label: 'Node 4b2' }, data: { label: 'Node 4b2' },
position: { x: 20, y: 100 }, position: { x: 20, y: 100 },
className: 'light', className: 'light',
parentNode: '4b', parentId: '4b',
}, },
{ {
id: '5', id: '5',
@@ -89,7 +92,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 5' }, data: { label: 'Node 5' },
position: { x: 650, y: 250 }, position: { x: 650, y: 250 },
className: 'light', className: 'light',
style: { width: 400, height: 150 }, style: { width: 100, height: 100 },
zIndex: 1000, zIndex: 1000,
}, },
{ {
@@ -97,15 +100,15 @@ const initialNodes: Node[] = [
data: { label: 'Node 5a' }, data: { label: 'Node 5a' },
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
className: 'light', className: 'light',
parentNode: '5', parentId: '5',
extent: 'parent', extent: 'parent',
}, },
{ {
id: '5b', id: '5b',
data: { label: 'Node 5b' }, data: { label: 'Node 5b' },
position: { x: 225, y: 50 }, position: { x: 200, y: 200 },
className: 'light', className: 'light',
parentNode: '5', parentId: '5',
expandParent: true, expandParent: true,
}, },
{ {
@@ -119,6 +122,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 3' }, data: { label: 'Node 3' },
position: { x: 400, y: 100 }, position: { x: 400, y: 100 },
className: 'light', className: 'light',
extent: 'parent',
}, },
]; ];
@@ -149,6 +153,7 @@ const nodeTypes = {
const Subflow = () => { const Subflow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null); const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const updateNodeInternals = useUpdateNodeInternals();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -158,10 +163,13 @@ const Subflow = () => {
const updatePos = () => { const updatePos = () => {
setNodes((nds) => { setNodes((nds) => {
return nds.map((n) => { return nds.map((n) => {
if (!n.parentNode) { if (!n.parentId) {
n.position = { return {
x: Math.random() * 400, ...n,
y: Math.random() * 400, position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
}; };
} }
@@ -176,8 +184,10 @@ const Subflow = () => {
const toggleClassnames = () => { const toggleClassnames = () => {
setNodes((nds) => { setNodes((nds) => {
return nds.map((n) => { return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light'; return {
return n; ...n,
className: n.className === 'light' ? 'dark' : 'light',
};
}); });
}); });
}; };
@@ -185,8 +195,10 @@ const Subflow = () => {
const toggleChildNodes = () => { const toggleChildNodes = () => {
setNodes((nds) => { setNodes((nds) => {
return nds.map((n) => { return nds.map((n) => {
n.hidden = !!n.parentNode && !n.hidden; return {
return n; ...n,
hidden: !!n.parentId && !n.hidden,
};
}); });
}); });
}; };
@@ -207,28 +219,27 @@ const Subflow = () => {
onlyRenderVisibleElements={false} onlyRenderVisibleElements={false}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
fitView fitView
nodeOrigin={[0, 0]}
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
<Background /> <Background />
<Panel position="top-right"> <Panel position="top-right">
<button onClick={resetTransform} style={{ marginRight: 5 }}> <button onClick={resetTransform}>reset transform</button>
reset transform <button onClick={updatePos}>change pos</button>
</button> <button onClick={toggleClassnames}>toggle classnames</button>
<button onClick={updatePos} style={{ marginRight: 5 }}> <button onClick={toggleChildNodes}>toggleChildNodes</button>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
toggleChildNodes
</button>
<button onClick={logToObject}>toObject</button> <button onClick={logToObject}>toObject</button>
<button onClick={() => setNodes(initialNodes)}>setNodes</button>
<button onClick={() => updateNodeInternals(nodes.map((node) => node.id))}>updateNodeInternals</button>
</Panel> </Panel>
</ReactFlow> </ReactFlow>
); );
}; };
export default Subflow; export default () => (
<ReactFlowProvider>
<Subflow />
</ReactFlowProvider>
);
@@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { ReactFlow, Node, Edge, useNodesState, useEdgesState, Position, Connection, addEdge } from '@xyflow/react'; import { ReactFlow, Node, Edge, useNodesState, useEdgesState, Position, Connection, addEdge } from '@xyflow/react';
import styles from './touch-device.module.css'; import './touch-device.css';
const initialNodes: Node[] = [ const initialNodes: Node[] = [
{ {
@@ -42,7 +42,7 @@ const TouchDeviceFlow = () => {
onConnectEnd={onConnectEnd} onConnectEnd={onConnectEnd}
onClickConnectStart={onClickConnectStart} onClickConnectStart={onClickConnectStart}
onClickConnectEnd={onClickConnectEnd} onClickConnectEnd={onClickConnectEnd}
className={styles.flow} className="touch-flow"
/> />
); );
}; };
@@ -0,0 +1,27 @@
.react-flow.touch-flow .react-flow__handle {
width: 20px;
height: 20px;
border-radius: 3px;
background-color: #9f7aea;
}
.touch-flow .react-flow__handle-right {
--translate: translate(50%, -50%);
}
.touch-flow .react-flow__handle-left {
--translate: translate(-50%, -50%);
}
@keyframes bounce {
0% {
transform: var(--translate) scale(1);
}
50% {
transform: var(--translate) scale(1.1);
}
}
.react-flow.touch-flow .react-flow__handle.clickconnecting {
animation: bounce 1600ms infinite ease-in;
}
@@ -1,19 +0,0 @@
.flow :global .react-flow__handle {
width: 20px;
height: 20px;
border-radius: 3px;
background-color: #9f7aea;
}
.flow :global .react-flow__handle.connecting {
animation: bounce 1600ms infinite ease-out;
}
@keyframes bounce {
0% {
transform: translate(0, -50%) scale(1);
}
50% {
transform: translate(0, -50%) scale(1.1);
}
}
@@ -10,7 +10,7 @@ import {
Edge, Edge,
ConnectionLineType, ConnectionLineType,
ConnectionMode, ConnectionMode,
updateEdge, reconnectEdge,
useNodesState, useNodesState,
useEdgesState, useEdgesState,
} from '@xyflow/react'; } from '@xyflow/react';
@@ -22,55 +22,55 @@ const initialNodes: Node[] = [
id: '00', id: '00',
type: 'custom', type: 'custom',
position: { x: 300, y: 250 }, position: { x: 300, y: 250 },
data: null, data: {},
}, },
{ {
id: '01', id: '01',
type: 'custom', type: 'custom',
position: { x: 100, y: 50 }, position: { x: 100, y: 50 },
data: null, data: {},
}, },
{ {
id: '02', id: '02',
type: 'custom', type: 'custom',
position: { x: 500, y: 50 }, position: { x: 500, y: 50 },
data: null, data: {},
}, },
{ {
id: '03', id: '03',
type: 'custom', type: 'custom',
position: { x: 500, y: 500 }, position: { x: 500, y: 500 },
data: null, data: {},
}, },
{ {
id: '04', id: '04',
type: 'custom', type: 'custom',
position: { x: 100, y: 500 }, position: { x: 100, y: 500 },
data: null, data: {},
}, },
{ {
id: '10', id: '10',
type: 'custom', type: 'custom',
position: { x: 300, y: 5 }, position: { x: 300, y: 5 },
data: null, data: {},
}, },
{ {
id: '20', id: '20',
type: 'custom', type: 'custom',
position: { x: 600, y: 250 }, position: { x: 600, y: 250 },
data: null, data: {},
}, },
{ {
id: '30', id: '30',
type: 'custom', type: 'custom',
position: { x: 300, y: 600 }, position: { x: 300, y: 600 },
data: null, data: {},
}, },
{ {
id: '40', id: '40',
type: 'custom', type: 'custom',
position: { x: 5, y: 250 }, position: { x: 5, y: 250 },
data: null, data: {},
}, },
]; ];
@@ -183,11 +183,11 @@ const getId = () => `${id++}`;
const UpdateNodeInternalsFlow = () => { const UpdateNodeInternalsFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const { project } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]); const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
const onEdgeUpdate = useCallback( const onReconnect = useCallback(
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)), (oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),
[] []
); );
@@ -196,12 +196,12 @@ const UpdateNodeInternalsFlow = () => {
setNodes((nds) => setNodes((nds) =>
nds.concat({ nds.concat({
id: getId(), id: getId(),
position: project({ x: evt.clientX, y: evt.clientY - 40 }), position: screenToFlowPosition({ x: evt.clientX, y: evt.clientY }),
type: 'custom', type: 'custom',
data: null, data: null,
}) })
), ),
[project, setNodes] [screenToFlowPosition, setNodes]
); );
return ( return (
@@ -215,7 +215,7 @@ const UpdateNodeInternalsFlow = () => {
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
connectionLineType={ConnectionLineType.Bezier} connectionLineType={ConnectionLineType.Bezier}
connectionMode={ConnectionMode.Loose} connectionMode={ConnectionMode.Loose}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
/> />
); );
}; };
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ReactFlow, Node, Edge, useNodesState, useEdgesState } from '@xyflow/react'; import { ReactFlow, Node, Edge, useNodesState, useEdgesState, ReactFlowProvider, useReactFlow } from '@xyflow/react';
import styles from './updatenode.module.css'; import styles from './updatenode.module.css';
@@ -13,6 +13,7 @@ const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const UpdateNode = () => { const UpdateNode = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const { updateNode } = useReactFlow();
const [nodeName, setNodeName] = useState<string>('Node 1'); const [nodeName, setNodeName] = useState<string>('Node 1');
const [nodeBg, setNodeBg] = useState<string>('#eee'); const [nodeBg, setNodeBg] = useState<string>('#eee');
@@ -23,9 +24,12 @@ const UpdateNode = () => {
nds.map((n) => { nds.map((n) => {
if (n.id === '1') { if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change // it's important that you create a new object here in order to notify react flow about the change
n.data = { return {
...n.data, ...n,
label: nodeName, data: {
...n.data,
label: nodeName,
},
}; };
} }
@@ -39,7 +43,10 @@ const UpdateNode = () => {
nds.map((n) => { nds.map((n) => {
if (n.id === '1') { if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change // it's important that you create a new object here in order to notify react flow about the change
n.style = { ...n.style, backgroundColor: nodeBg }; return {
...n,
style: { ...n.style, backgroundColor: nodeBg },
};
} }
return n; return n;
@@ -51,8 +58,10 @@ const UpdateNode = () => {
setNodes((nds) => setNodes((nds) =>
nds.map((n) => { nds.map((n) => {
if (n.id === '1' || n.id === 'e1-2') { if (n.id === '1' || n.id === 'e1-2') {
// when you update a simple type you can just update the value return {
n.hidden = nodeHidden; ...n,
hidden: nodeHidden,
};
} }
return n; return n;
@@ -80,9 +89,19 @@ const UpdateNode = () => {
<label>hidden:</label> <label>hidden:</label>
<input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} /> <input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} />
</div> </div>
<button
onClick={() => updateNode('1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }))}
>
update position
</button>
</div> </div>
</ReactFlow> </ReactFlow>
); );
}; };
export default UpdateNode; export default () => (
<ReactFlowProvider>
<UpdateNode />
</ReactFlowProvider>
);
@@ -0,0 +1,75 @@
import { useCallback, useEffect } from 'react';
import {
ReactFlow,
Background,
MiniMap,
Node,
addEdge,
ReactFlowProvider,
Edge,
useNodesState,
useEdgesState,
OnConnect,
useConnection,
} from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
];
const UseZoomPanHelperFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect: OnConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []);
const connection = useConnection();
useEffect(() => {
console.log('connection', connection);
}, [connection]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
>
<Background />
<MiniMap />
</ReactFlow>
);
};
const WrappedFlow = () => (
<ReactFlowProvider>
<UseZoomPanHelperFlow />
</ReactFlowProvider>
);
export default WrappedFlow;
@@ -0,0 +1,39 @@
import { memo, FC, useEffect, useCallback } from 'react';
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleProps } from '@xyflow/react';
function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }) {
const onConnect = useCallback(
(connections: Connection[]) => console.log('onConnect handler, node id:', nodeId, connections),
[nodeId]
);
const onDisconnect = useCallback(
(connections: Connection[]) => console.log('onDisconnect handler, node id:', nodeId, connections),
[nodeId]
);
const connections = useHandleConnections({
type: handleProps.type,
id: handleProps.id,
onConnect,
onDisconnect,
});
useEffect(() => {
console.log('useEffect, node id:', nodeId, handleProps.type, connections);
}, [connections]);
return <Handle {...handleProps} />;
}
const CustomNode: FC<NodeProps> = ({ id }) => {
return (
<div style={{ background: '#333', color: '#fff', padding: 10, fontSize: 12, borderRadius: 10 }}>
<CustomHandle nodeId={id} type="target" position={Position.Left} />
<div>node {id}</div>
<CustomHandle nodeId={id} type="source" position={Position.Right} id="a" style={{ top: 10 }} />
<CustomHandle nodeId={id} type="source" position={Position.Right} id="b" style={{ top: 20 }} />
</div>
);
};
export default memo(CustomNode);
@@ -0,0 +1,41 @@
import { memo, FC, useEffect, useCallback } from 'react';
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleProps } from '@xyflow/react';
function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }) {
const onConnect = useCallback(
(connections: Connection[]) => {
console.log('onConnect handler, node id:', nodeId, connections);
},
[nodeId]
);
const onDisconnect = useCallback(
(connections: Connection[]) => {
console.log('onDisconnect handler, node id:', nodeId, connections);
},
[nodeId]
);
const connections = useHandleConnections({
type: handleProps.type,
id: handleProps.id,
onConnect,
onDisconnect,
});
useEffect(() => {
console.log('useEffect, node id:', nodeId, handleProps.type, connections);
}, [connections]);
return <Handle {...handleProps} />;
}
const CustomNode: FC<NodeProps> = ({ id }) => {
return (
<div style={{ background: '#333', color: '#fff', padding: 10, fontSize: 12, borderRadius: 10 }}>
<CustomHandle nodeId={id} type="target" position={Position.Left} />
<div>node {id}</div>
<CustomHandle nodeId={id} type="source" position={Position.Right} />
</div>
);
};
export default memo(CustomNode);
@@ -0,0 +1,118 @@
import { useCallback } from 'react';
import {
ReactFlow,
MiniMap,
Controls,
addEdge,
Connection,
useNodesState,
useEdgesState,
Background,
} from '@xyflow/react';
import MultiHandleNode from './MultiHandleNode';
import SingleHandleNode from './SingleHandleNode';
const nodeTypes = {
multi: MultiHandleNode,
single: SingleHandleNode,
};
const initNodes = [
{
id: '1',
type: 'single',
data: {},
position: { x: 0, y: 0 },
},
{
id: '2',
type: 'single',
data: {},
position: { x: 200, y: -100 },
},
{
id: '3',
type: 'single',
data: {},
position: { x: 200, y: 100 },
},
{
id: '4',
type: 'multi',
data: {},
position: { x: 400, y: 0 },
},
{
id: '5',
type: 'multi',
data: {},
position: { x: 600, y: -100 },
},
{
id: '6',
type: 'multi',
data: {},
position: { x: 600, y: 100 },
},
];
const initEdges = [
{
id: 'e1-2',
source: '1',
target: '2',
},
{
id: 'e1-3',
source: '1',
target: '3',
},
{
id: 'e4a-5',
source: '4',
sourceHandle: 'a',
target: '5',
},
{
id: 'e4b-5',
source: '4',
sourceHandle: 'b',
target: '6',
},
];
const defaultEdgeOptions = {
animated: true,
};
const CustomNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
minZoom={0.3}
maxZoom={2}
colorMode="dark"
defaultEdgeOptions={defaultEdgeOptions}
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
);
};
export default CustomNodeFlow;
@@ -1,9 +1,12 @@
import { useKeyPress } from '@xyflow/react'; import { useKeyPress } from '@xyflow/react';
import { useEffect } from 'react';
const UseKeyPressComponent = () => { const UseKeyPressComponent = () => {
const metaPressed = useKeyPress(['Meta']); const metaPressed = useKeyPress(['Meta']);
console.log({ metaPressed }); useEffect(() => {
console.log({ metaPressed });
}, [metaPressed]);
return <div />; return <div />;
}; };
@@ -0,0 +1,20 @@
import { memo } from 'react';
import { Handle, Position, useHandleConnections, useNodesData } from '@xyflow/react';
import { isTextNode, type MyNode } from '.';
function ResultNode() {
const connections = useHandleConnections({
type: 'target',
});
const nodesData = useNodesData<MyNode>(connections.map((connection) => connection.source));
const textNodes = nodesData.filter(isTextNode);
return (
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
<Handle type="target" position={Position.Left} />
<div>incoming texts: {textNodes.map(({ data }, i) => <div key={i}>{data.text}</div>) || 'none'}</div>
</div>
);
}
export default memo(ResultNode);
@@ -0,0 +1,33 @@
import { memo, useState } from 'react';
import { Position, NodeProps, Handle, useReactFlow } from '@xyflow/react';
function TextNode({ id, data }: NodeProps) {
const { updateNodeData } = useReactFlow();
const [text, setText] = useState(data.text);
const updateText = (text: string) => {
// avoid jumping caret with a synchronous update
setText(text);
// update actual node data
updateNodeData(id, { text });
};
return (
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
<div>node {id}</div>
<div style={{ marginTop: 5 }}>
<label style={{ fontSize: 12 }}>useState + updateNodeData</label>
<input onChange={(evt) => updateText(evt.target.value)} value={text} style={{ display: 'block' }} />
<label style={{ fontSize: 12 }}>updateNodeData</label>
<input
onChange={(evt) => updateNodeData(id, { text: evt.target.value })}
value={data.text}
style={{ display: 'block' }}
/>
</div>
<Handle type="source" position={Position.Right} />
</div>
);
}
export default memo(TextNode);
@@ -0,0 +1,26 @@
import { memo, useEffect } from 'react';
import { Position, NodeProps, useReactFlow, Handle, useHandleConnections, useNodesData } from '@xyflow/react';
import { isTextNode, type TextNode, type MyNode } from '.';
function UppercaseNode({ id }: NodeProps) {
const { updateNodeData } = useReactFlow();
const connections = useHandleConnections({
type: 'target',
});
const nodesData = useNodesData<MyNode>(connections[0]?.source);
const textNode = isTextNode(nodesData) ? nodesData : null;
useEffect(() => {
updateNodeData(id, { text: textNode?.data.text.toUpperCase() });
}, [textNode]);
return (
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
<Handle type="target" position={Position.Left} isConnectable={connections.length === 0} />
<div>uppercase transform</div>
<Handle type="source" position={Position.Right} />
</div>
);
}
export default memo(UppercaseNode);
@@ -0,0 +1,126 @@
import { useCallback } from 'react';
import {
ReactFlow,
Controls,
addEdge,
Connection,
useNodesState,
useEdgesState,
Background,
Node,
Edge,
} from '@xyflow/react';
import TextNode from './TextNode';
import ResultNode from './ResultNode';
import UppercaseNode from './UppercaseNode';
export type TextNode = Node<{ text: string }, 'text'>;
export type ResultNode = Node<{}, 'result'>;
export type UppercaseNode = Node<{ text: string }, 'uppercase'>;
export type MyNode = TextNode | ResultNode | UppercaseNode;
export function isTextNode(node: any): node is TextNode | UppercaseNode {
return node.type === 'text' || node.type === 'uppercase';
}
const nodeTypes = {
text: TextNode,
result: ResultNode,
uppercase: UppercaseNode,
};
const initNodes: MyNode[] = [
{
id: '1',
type: 'text',
data: {
text: 'hello',
},
position: { x: -100, y: -50 },
},
{
id: '1a',
type: 'uppercase',
data: { text: '' },
position: { x: 100, y: 0 },
},
{
id: '1b',
type: 'uppercase',
data: { text: '' },
position: { x: 100, y: -100 },
},
{
id: '2',
type: 'text',
data: {
text: 'world',
},
position: { x: 0, y: 100 },
},
{
id: '3a',
type: 'result',
data: {},
position: { x: 300, y: -75 },
},
{
id: '3b',
type: 'result',
data: {},
position: { x: 300, y: 50 },
},
];
const initEdges: Edge[] = [
{
id: 'e1-1a',
source: '1',
target: '1a',
},
{
id: 'e1a-3a',
source: '1b',
target: '3a',
},
{
id: 'e1-1b',
source: '1',
target: '1b',
},
{
id: 'e1a-3b',
source: '1a',
target: '3b',
},
{
id: 'e2-3b',
source: '2',
target: '3b',
},
];
const CustomNodeFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
>
<Controls />
<Background />
</ReactFlow>
);
};
export default CustomNodeFlow;
@@ -11,6 +11,8 @@ import {
useEdgesState, useEdgesState,
OnConnect, OnConnect,
useNodesInitialized, useNodesInitialized,
Panel,
useReactFlow,
} from '@xyflow/react'; } from '@xyflow/react';
const initialNodes: Node[] = [ const initialNodes: Node[] = [
@@ -49,6 +51,7 @@ const initialEdges: Edge[] = [
]; ];
const UseZoomPanHelperFlow = () => { const UseZoomPanHelperFlow = () => {
const { addNodes } = useReactFlow();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -59,6 +62,13 @@ const UseZoomPanHelperFlow = () => {
console.log('initialized', initialized); console.log('initialized', initialized);
}, [initialized]); }, [initialized]);
const addNode = () =>
addNodes({
id: `${Math.random()}`,
data: { label: 'new node' },
position: { x: Math.random() * 400, y: Math.random() * 400 },
});
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -71,6 +81,9 @@ const UseZoomPanHelperFlow = () => {
> >
<Background /> <Background />
<MiniMap /> <MiniMap />
<Panel>
<button onClick={addNode}>add node</button>
</Panel>
</ReactFlow> </ReactFlow>
); );
}; };
@@ -1,4 +1,4 @@
import { useCallback } from 'react'; import { useCallback, useState } from 'react';
import { import {
ReactFlow, ReactFlow,
addEdge, addEdge,
@@ -10,6 +10,7 @@ import {
useEdgesState, useEdgesState,
useOnSelectionChange, useOnSelectionChange,
OnSelectionChangeParams, OnSelectionChangeParams,
OnSelectionChangeFunc,
} from '@xyflow/react'; } from '@xyflow/react';
const initialNodes: Node[] = [ const initialNodes: Node[] = [
@@ -35,9 +36,9 @@ const initialEdges: Edge[] = [
}, },
]; ];
const SelectionLogger = () => { const SelectionLogger = ({ id }: { id: string }) => {
const onChange = useCallback(({ nodes, edges }: OnSelectionChangeParams) => { const onChange = useCallback(({ nodes, edges }: OnSelectionChangeParams) => {
console.log(nodes, edges); console.log(id, nodes, edges);
}, []); }, []);
useOnSelectionChange({ useOnSelectionChange({
@@ -63,14 +64,23 @@ const Flow = () => {
); );
}; };
const WrappedFlow = () => ( const WrappedFlow = () => {
<ReactFlowProvider> const [secondLoggerActive, setSecondLoggerActive] = useState<boolean>(true);
<Flow />
<SelectionLogger /> const toggleSecondLogger = () => {
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}> setSecondLoggerActive(!secondLoggerActive);
<input type={'text'} placeholder={'name'} /> };
</div>
</ReactFlowProvider> return (
); <ReactFlowProvider>
<Flow />
<SelectionLogger id="Logger 1" />
{secondLoggerActive && <SelectionLogger id="Logger 2" />}
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={toggleSecondLogger}>{secondLoggerActive ? 'Disable' : 'Enable'} Logger 2</button>
</div>
</ReactFlowProvider>
);
};
export default WrappedFlow; export default WrappedFlow;
@@ -1,4 +1,4 @@
import { useCallback, MouseEvent, useEffect } from 'react'; import { useCallback, MouseEvent, useEffect, useRef } from 'react';
import { import {
ReactFlow, ReactFlow,
Background, Background,
@@ -27,18 +27,21 @@ const initialNodes: Node[] = [
data: { label: 'Node 2' }, data: { label: 'Node 2' },
position: { x: 100, y: 100 }, position: { x: 100, y: 100 },
className: 'light', className: 'light',
type: 'default',
}, },
{ {
id: '3', id: '3',
data: { label: 'Node 3' }, data: { label: 'Node 3' },
position: { x: 400, y: 100 }, position: { x: 400, y: 100 },
className: 'light', className: 'light',
type: 'default',
}, },
{ {
id: '4', id: '4',
data: { label: 'Node 4' }, data: { label: 'Node 4' },
position: { x: 400, y: 200 }, position: { x: 400, y: 200 },
className: 'light', className: 'light',
type: 'default',
}, },
]; ];
@@ -57,7 +60,7 @@ const UseZoomPanHelperFlow = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)); const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const { const {
project, screenToFlowPosition,
setCenter, setCenter,
zoomIn, zoomIn,
zoomOut, zoomOut,
@@ -68,13 +71,14 @@ const UseZoomPanHelperFlow = () => {
getNodes, getNodes,
getEdges, getEdges,
deleteElements, deleteElements,
updateNodeData,
} = useReactFlow(); } = useReactFlow();
const onPaneClick = useCallback( const onPaneClick = useCallback(
(evt: MouseEvent) => { (evt: MouseEvent) => {
const projectedPosition = project({ const projectedPosition = screenToFlowPosition({
x: evt.clientX, x: evt.clientX,
y: evt.clientY - 40, y: evt.clientY,
}); });
setNodes((nds) => setNodes((nds) =>
@@ -84,10 +88,11 @@ const UseZoomPanHelperFlow = () => {
data: { data: {
label: `${projectedPosition.x}-${projectedPosition.y}`, label: `${projectedPosition.x}-${projectedPosition.y}`,
}, },
type: 'default',
}) })
); );
}, },
[project, setNodes] [screenToFlowPosition, setNodes]
); );
const onNodeClick = useCallback( const onNodeClick = useCallback(
@@ -105,6 +110,7 @@ const UseZoomPanHelperFlow = () => {
data: { data: {
label: 'New Node', label: 'New Node',
}, },
type: 'default',
}; };
addNodes(newNode); addNodes(newNode);
@@ -125,12 +131,31 @@ const UseZoomPanHelperFlow = () => {
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] }); deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
}, []); }, []);
const edgeAdded = useRef(false);
useEffect(() => { useEffect(() => {
addEdges({ id: 'e3-4', source: '3', target: '4' }); if (!edgeAdded.current) {
addEdges({ id: 'e3-4', source: '3', target: '4' });
edgeAdded.current = true;
}
}, [addEdges]); }, [addEdges]);
const onResetNodes = useCallback(() => setNodesHook(initialNodes), [setNodesHook]); const onResetNodes = useCallback(() => setNodesHook(initialNodes), [setNodesHook]);
const onSetNodes = () => {
setNodes([
{ id: 'a', type: 'default', position: { x: 0, y: 0 }, data: { label: 'Node a' } },
{ id: 'b', type: 'default', position: { x: 0, y: 150 }, data: { label: 'Node b' } },
]);
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
};
const onUpdateNode = () => {
updateNodeData('1', { label: 'update' });
updateNodeData('2', { label: 'update' });
};
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -153,6 +178,8 @@ const UseZoomPanHelperFlow = () => {
<button onClick={logNodes}>useNodes</button> <button onClick={logNodes}>useNodes</button>
<button onClick={deleteSelectedElements}>deleteSelectedElements</button> <button onClick={deleteSelectedElements}>deleteSelectedElements</button>
<button onClick={deleteSomeElements}>deleteSomeElements</button> <button onClick={deleteSomeElements}>deleteSomeElements</button>
<button onClick={onSetNodes}>setNodes</button>
<button onClick={onUpdateNode}>updateNode</button>
</Panel> </Panel>
<Background /> <Background />
<MiniMap /> <MiniMap />
@@ -33,23 +33,23 @@ const getId = (): string => `${id++}`;
const UpdateNodeInternalsFlow = () => { const UpdateNodeInternalsFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]); const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
const { project } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
const onPaneClick = useCallback( const onPaneClick = useCallback(
(evt: MouseEvent) => (evt: MouseEvent) =>
setNodes((nds) => setNodes((nds) =>
nds.concat({ nds.concat({
id: getId(), id: getId(),
position: project({ x: evt.clientX, y: evt.clientY - 40 }), position: screenToFlowPosition({ x: evt.clientX, y: evt.clientY }),
data: { label: 'new node' }, data: { label: 'new node' },
targetPosition: Position.Left, targetPosition: Position.Left,
sourcePosition: Position.Right, sourcePosition: Position.Right,
}) })
), ),
[project, setNodes] [screenToFlowPosition, setNodes]
); );
return ( return (

Some files were not shown because too many files have changed in this diff Show More