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",
"changelog": ["@changesets/changelog-github", { "repo": "wbkd/react-flow" }],
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "xyflow/xyflow" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
"ignore": ["react-examples", "svelte-examples", "astro-examples", "playwright"]
}
+38 -42
View File
@@ -4,47 +4,57 @@ body:
- type: markdown
attributes:
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)
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)
**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.
Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:
- 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
**Issue with the xyflow Docs?** Submit an issue in our [web repo](https://github.com/xyflow/web/issues) instead.
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
id: description
id: platform
attributes:
label: Describe the Bug
description: Provide a clear and concise description of the challenge you are running into.
label: What platform were you using when you found the bug?
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:
required: true
- type: input
id: link
attributes:
label: Your Example Website or App
label: Live code example
description: |
Which website or app were you using when the bug happened?
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.
- To create a shareable code example you can use Codesandbox (https://codesandbox.io/s/react-flow-starter-ou8or) or Stackblitz (https://stackblitz.com/).
- Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve.
placeholder: |
e.g. https://stackblitz.com/edit/...... OR Github Repo
This is the most important part of the bug report!
React Flow codesandbox starters
- js: https://new.reactflow.dev/js
- ts: https://new.reactflow.dev/ts
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:
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
id: steps
attributes:
label: Steps to Reproduce the Bug or Issue
description: Describe the steps we have to take to reproduce the behavior.
label: Steps to reproduce the bug or issue
description: How can we see the reported behavior in the code example you provided? The more detail, the better!
placeholder: |
1. Go to '...'
2. Click on '....'
@@ -56,9 +66,8 @@ body:
id: expected
attributes:
label: Expected behavior
description: Provide a clear and concise description of what you expected to happen.
placeholder: |
As a user, I expected ___ behavior but i am seeing ___
description: What would you expect to happen if there was no bug or issue?
placeholder: As a user, I expected ___ behavior, but instead I am seeing ___
validations:
required: true
- type: textarea
@@ -67,22 +76,9 @@ body:
label: Screenshots or Videos
description: |
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
to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
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
Supported image/file types and file size limits- 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
- type: textarea
id: additional
attributes:
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
strict-peer-dependencies=false
+1 -1
View File
@@ -1,6 +1,6 @@
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
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 repository is the home of four packages:
* React Flow v11 `reactflow` [v11 branch](https://github.com/xyflow/xyflow/tree/v11)
* React Flow v12 (not published yet) [packages/react](./packages/react)
* React Flow 11 `reactflow` [v11 branch](https://github.com/xyflow/xyflow/tree/v11)
* React Flow 12 (beta) `@xyflow/react` [packages/react](./packages/react)
* Svelte Flow `@xyflow/svelte` [packages/svelte](./packages/svelte)
* 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`.
## 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 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>
<summary><strong>React Flow</strong> basic usage</summary>
@@ -151,6 +151,15 @@ npm install @xyflow/svelte
```
</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
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",
"private": true,
"version": "0.0.1",
@@ -7,7 +7,7 @@ const sourceHandleStyleB: CSSProperties = {
left: 'auto',
};
const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
const CustomNode: FC<NodeProps> = ({ data, positionAbsoluteX, positionAbsoluteY }) => {
return (
<>
<Handle type="target" position={Position.Top} />
@@ -18,7 +18,7 @@ const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
<div>
Position:{' '}
<strong>
{xPos.toFixed(2)},{yPos.toFixed(2)}
{positionAbsoluteX.toFixed(2)},{positionAbsoluteY.toFixed(2)}
</strong>
</div>
</div>
@@ -27,7 +27,7 @@ const initialNodes: Node[] = [
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
size: nodeSize,
...nodeSize,
handles: [
{
type: 'source',
@@ -41,23 +41,19 @@ const initialNodes: Node[] = [
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
size: nodeSize,
...nodeSize,
handles: [
{
type: 'source',
position: Position.Bottom,
x: nodeSize.width * 0.5,
y: nodeSize.height,
width: 1,
height: 1,
},
{
type: 'target',
position: Position.Top,
x: nodeSize.width * 0.5,
y: 0,
width: 1,
height: 1,
},
],
},
@@ -65,23 +61,19 @@ const initialNodes: Node[] = [
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
size: nodeSize,
...nodeSize,
handles: [
{
type: 'source',
position: Position.Bottom,
x: nodeSize.width * 0.5,
y: nodeSize.height,
width: 1,
height: 1,
},
{
type: 'target',
position: Position.Top,
x: nodeSize.width * 0.5,
y: 0,
width: 1,
height: 1,
},
],
},
@@ -89,6 +81,8 @@ const initialNodes: Node[] = [
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
width: 200,
height: 50,
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' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
size: {
width: 100,
height: 40,
},
width: 100,
height: 40,
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
@@ -26,10 +24,8 @@
data: { label: 'A' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
size: {
width: 100,
height: 40,
},
width: 100,
height: 40,
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
@@ -41,10 +37,8 @@
data: { label: 'B' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
size: {
width: 100,
height: 40,
},
width: 100,
height: 40,
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
@@ -56,10 +50,8 @@
data: { label: 'C' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
size: {
width: 100,
height: 40,
},
width: 100,
height: 40,
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ 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 SvelteFlowApp from '../components/SvelteFlowExample/index.svelte'
import ReactFlowApp from '../components/ReactFlowExample';
import ReactFlowInitialApp from '../components/ReactFlowInitialExample';
import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte';
import SvelteFlowInitialApp from '../components/SvelteFlowInitialExample/index.svelte';
---
<html lang="en">
@@ -18,10 +20,24 @@ import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte'
</style>
</head>
<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>
<SvelteFlowApp />
<h2>React Flow</h2>
<ReactFlowApp />
<p>client hydration on load (client:load)</p>
<SvelteFlowApp client:load />
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
<SvelteFlowInitialApp client:load />
</body>
</html>
+11 -1
View File
@@ -1,6 +1,6 @@
# 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
@@ -8,5 +8,15 @@ This Vite app is used internally to test the library.
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({
e2e: {
baseUrl: 'http://localhost:3000',
baseUrl: 'http://localhost:3000/examples',
viewportWidth: 1280,
viewportHeight: 720,
video: false,
@@ -13,7 +13,6 @@ describe('useEdges.cy.tsx', () => {
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
});
@@ -15,8 +15,9 @@ const initialNodes: Node[] = nodes.map((n) => ({
const expectedNodes: Node[] = initialNodes.map((n) => ({
...n,
positionAbsolute: n.position,
...nodeDimensions,
measured: {
...nodeDimensions,
},
}));
describe('useNodes.cy.tsx', () => {
@@ -29,7 +30,6 @@ describe('useNodes.cy.tsx', () => {
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
});
@@ -6,7 +6,7 @@ import * as simpleflow from '../../fixtures/simpleflow';
describe('<ReactFlow />: Basic Props', () => {
describe('uses defaultNodes and defaultEdges', () => {
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', () => {
@@ -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', () => {
cy.get('body')
.type('{shift}', { release: false })
.wait(50)
.type('{Shift}', { release: false })
.get('.react-flow__pane')
.trigger('mousedown', 1, 10, { button: 0, force: true })
.trigger('mousemove', 1000, 450, { button: 0 })
.wait(50)
.trigger('mouseup', 1000, 450, { button: 0, force: true });
.trigger('mousemove', 1000, 200, { button: 0 })
.trigger('mouseup', 1000, 200, { button: 0 });
cy.wait(200);
cy.get('.react-flow__node').eq(1).should('have.class', 'selected');
cy.get('.react-flow__node').eq(0).should('have.class', 'selected');
cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected');
cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true, force: true });
});
@@ -97,19 +92,19 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
it('drags a node', () => {
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');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
// @TODO: why does this fail since react18?
// it('removes a node', () => {
// cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
// cy.get('html').type('{backspace}').wait(100);
// cy.get('.react-flow__node').should('have.length', 3);
// cy.get('.react-flow__edge').should('have.length', 1);
// });
it('removes a node', () => {
cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
cy.get('html').realPress('Backspace');
cy.get('.react-flow__node').should('have.length', 3);
cy.get('.react-flow__edge').should('have.length', 0);
});
it('connects nodes', () => {
cy.get('.react-flow__node')
@@ -125,16 +120,15 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
.trigger('mouseup', { force: true, button: 0 });
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', () => {
// cy.get('.react-flow__edge:first').click();
// cy.get('body').type('{backspace}');
it('removes an edge', () => {
cy.get('.react-flow__edge:first').click();
cy.get('html').realPress('Backspace');
// cy.get('.react-flow__edge').should('have.length', 1);
// });
cy.get('.react-flow__edge').should('have.length', 0);
});
it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
@@ -59,7 +59,7 @@ function ControlledFlow({
handlers.onConnect = onConnect;
}
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} />;
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} nodeDragThreshold={0} />;
}
export default ControlledFlow;
+1 -1
View File
@@ -9,7 +9,7 @@ Cypress.Commands.add('drag', (selector, { x, y }) =>
const nextY: number = centerY + y;
return elementToDrag
.trigger('mousedown', { view: window })
.trigger('mousedown', { view: window, force: true })
.trigger('mousemove', nextX, nextY, { force: true })
.wait(50)
.trigger('mouseup', { view: window, force: true });
+1
View File
@@ -15,6 +15,7 @@
// Import commands.js using ES2015 syntax:
import './commands';
import 'cypress-real-events/support';
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<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" />
<title>React Flow Examples</title>
</head>
+17 -14
View File
@@ -1,5 +1,5 @@
{
"name": "@xyflow/react-examples",
"name": "react-examples",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -14,26 +14,29 @@
"test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'"
},
"dependencies": {
"@reduxjs/toolkit": "^2.2.3",
"@xyflow/react": "workspace:*",
"classcat": "^5.0.3",
"classcat": "^5.0.4",
"dagre": "^0.8.5",
"localforage": "^1.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"zustand": "^4.4.1"
"react-redux": "^9.1.1",
"react-router-dom": "^6.18.0",
"redux": "^5.0.1",
"zustand": "^4.4.6"
},
"devDependencies": {
"@cypress/skip-test": "^2.6.1",
"@types/dagre": "^0.7.48",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "4.0.4",
"@vitejs/plugin-react-swc": "^3.3.2",
"cypress": "12.17.3",
"cypress-real-events": "1.10.0",
"start-server-and-test": "^1.14.0",
"typescript": "5.1.3",
"vite": "4.4.9"
"@types/dagre": "^0.7.52",
"@types/react": "^18.2.36",
"@types/react-dom": "^18.2.14",
"@vitejs/plugin-react": "4.1.1",
"@vitejs/plugin-react-swc": "^3.4.1",
"cypress": "13.6.6",
"cypress-real-events": "1.12.0",
"start-server-and-test": "^2.0.2",
"typescript": "5.2.2",
"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, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import routes from './routes';
import GenericTestRoutes from '../generic-tests';
import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds';
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>
);
};
import Header from './header';
export default () => (
<BrowserRouter>
<Header />
<Routes>
{routes.map((route) => (
<Route path={route.path} key={route.path} element={<route.component />} />
))}
<Route path="/" element={<Navigate to="/examples" />} />
<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>
</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 {
ReactFlow,
MiniMap,
@@ -10,12 +10,16 @@ import {
Edge,
useReactFlow,
Panel,
OnNodeDrag,
} from '@xyflow/react';
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
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 printSelectionEvent = (name: string) => (_: MouseEvent, nodes: Node[]) => console.log(name, nodes);
const initialNodes: Node[] = [
{
id: '1',
@@ -52,41 +56,80 @@ const initialEdges: Edge[] = [
const defaultEdgeOptions = {};
const BasicFlow = () => {
const instance = useReactFlow();
const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
useReactFlow();
const updatePos = () => {
instance.setNodes((nodes) =>
setNodes((nodes) =>
nodes.map((node) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
return {
...node,
position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
};
return node;
})
);
};
const logToObject = () => console.log(instance.toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const logToObject = () => console.log(toObject());
const resetTransform = () => setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
instance.setNodes((nodes) =>
setNodes((nodes) =>
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 (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onSelectionDragStart={printSelectionEvent('selection drag start')}
onSelectionDrag={printSelectionEvent('selection drag')}
onSelectionDragStop={printSelectionEvent('selection drag stop')}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
@@ -95,6 +138,7 @@ const BasicFlow = () => {
selectNodesOnDrag={false}
elevateEdgesOnSelect
elevateNodesOnSelect={false}
nodeDragThreshold={0}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
@@ -105,6 +149,12 @@ const BasicFlow = () => {
<button onClick={updatePos}>change pos</button>
<button onClick={toggleClassnames}>toggle classnames</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>
</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 = () => {
instance.setNodes((nodes) =>
nodes.map((node) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
return {
...node,
position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
};
return node;
})
);
};
@@ -73,11 +74,12 @@ const ControlledUncontrolled = () => {
const updateEdgeColors = () => {
instance.setEdges((edges) =>
edges.map((edge) => {
edge.style = {
stroke: '#ff5050',
return {
...edge,
style: {
stroke: '#ff5050',
},
};
return edge;
})
);
};
@@ -27,13 +27,13 @@ const buttonStyle: CSSProperties = {
zIndex: 4,
};
const CustomMiniMapNode = ({ x, y, width, height, color }: MiniMapNodeProps) => (
<circle cx={x} cy={y} r={Math.max(width, height) / 2} fill={color} />
);
const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => {
return <circle cx={x} cy={y} r={Math.max(width, height) / 2} fill="#ffcc00" />;
};
const CustomMiniMapNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
const addRandomNode = () => {
@@ -59,7 +59,7 @@ const CustomMiniMapNodeFlow = () => {
onNodeClick={onNodeClick}
onConnect={(p) => onConnect(p)}
onNodeDragStop={onNodeDragStop}
onlyRenderVisibleElements={false}
onlyRenderVisibleElements={true}
>
<Controls />
<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 type { ColorSelectorNode } from '.';
const targetHandleStyle: CSSProperties = { background: '#555' };
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
const sourceHandleStyleB: CSSProperties = {
@@ -11,7 +13,7 @@ const sourceHandleStyleB: CSSProperties = {
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 onChange = useCallback((viewport: Viewport) => console.log('onChange', 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} />
</>
);
};
}
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 {
ReactFlow,
MiniMap,
Controls,
addEdge,
Node,
ReactFlowInstance,
Position,
SnapGrid,
Connection,
useNodesState,
useEdgesState,
Background,
OnNodeDrag,
OnInit,
applyNodeChanges,
OnNodesChange,
OnConnect,
OnBeforeDelete,
BuiltInNode,
BuiltInEdge,
NodeTypes,
ReactFlowProvider,
} from '@xyflow/react';
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);
};
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onNodeDragStop: OnNodeDrag<MyNode> = (_, node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: MyNode) => console.log('click', node);
const initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16];
const nodeTypes = {
const nodeTypes: NodeTypes = {
selectorNode: ColorSelectorNode,
};
const CustomNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const ref = useRef(null);
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);
@@ -42,7 +66,7 @@ const CustomNodeFlow = () => {
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setNodes((nds) =>
nds.map((node) => {
if (node.id !== '2') {
if (node.id !== '2' || node.type !== 'selectorNode') {
return node;
}
@@ -119,12 +143,13 @@ const CustomNodeFlow = () => {
]);
}, []);
const onConnect = useCallback(
(connection: Connection) =>
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
const onConnect: OnConnect = useCallback(
(connection) => setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
[setEdges]
);
const onBeforeDelete: OnBeforeDelete<MyNode, MyEdge> = useCallback(async (params) => true, []);
return (
<ReactFlow
nodes={nodes}
@@ -142,16 +167,18 @@ const CustomNodeFlow = () => {
fitView
minZoom={0.3}
maxZoom={2}
onBeforeDelete={onBeforeDelete}
ref={ref}
>
<MiniMap
nodeStrokeColor={(n: Node): string => {
<MiniMap<MyNode>
nodeStrokeColor={(n: MyNode): string => {
if (n.type === 'input') return '#0041d0';
if (n.type === 'selectorNode') return bgColor;
if (n.type === 'output') return '#ff0072';
return '#eee';
}}
nodeColor={(n: Node): string => {
nodeColor={(n: MyNode): string => {
if (n.type === 'selectorNode') return bgColor;
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',
type: 'output',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
@@ -54,26 +55,24 @@ const DefaultNodes = () => {
const updateNodePositions = () => {
instance.setNodes((nodes) =>
nodes.map((node) => {
node.position = {
nodes.map((node) => ({
...node,
position: {
x: Math.random() * 400,
y: Math.random() * 400,
};
return node;
})
},
}))
);
};
const updateEdgeColors = () => {
instance.setEdges((edges) =>
edges.map((edge) => {
edge.style = {
edges.map((edge) => ({
...edge,
style: {
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',
style: { border: '1px solid #ddd', padding: '20px 40px' },
position: { x: 200, y: 200 },
data: null,
data: {},
},
];
@@ -33,6 +33,7 @@ const DragHandleFlow = () => {
edges={edges}
nodeTypes={nodeTypes}
onNodeClick={onNodeClick}
nodeDragThreshold={0}
/>
);
};
@@ -39,7 +39,7 @@ const nodeOrigin: NodeOrigin = [0.5, 0.5];
const DnDFlow = () => {
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
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 onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi);
@@ -49,7 +49,7 @@ const DnDFlow = () => {
if (reactFlowInstance) {
const type = event.dataTransfer.getData('application/reactflow');
const position = reactFlowInstance.screenToFlowCoordinate({
const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
@@ -66,7 +66,7 @@ const DnDFlow = () => {
return (
<div className={styles.dndflow}>
<ReactFlowProvider nodes={initialNodes} edges={[]}>
<ReactFlowProvider initialNodes={initialNodes} initialEdges={[]}>
<div className={styles.wrapper}>
<ReactFlow
nodes={nodes}
@@ -77,6 +77,7 @@ const DnDFlow = () => {
onInit={onInit}
onDrop={onDrop}
onDragOver={onDragOver}
nodeOrigin={nodeOrigin}
>
<Controls />
</ReactFlow>
@@ -1,11 +1,11 @@
import { useCallback } from 'react';
import { useStore, getStraightPath, EdgeProps } from '@xyflow/react';
import { useStore, getStraightPath, EdgeProps, useInternalNode } from '@xyflow/react';
import { getEdgeParams } from './utils.js';
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
const sourceNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === source), [source]));
const targetNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === target), [target]));
const sourceNode = useInternalNode(source);
const targetNode = useInternalNode(target);
if (!sourceNode || !targetNode) {
return null;
@@ -5,7 +5,6 @@ import CustomNode from './CustomNode';
import FloatingEdge from './FloatingEdge';
import CustomConnectionLine from './CustomConnectionLine';
import '@xyflow/react/dist/style.css';
import './style.css';
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
// 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
const {
width: intersectionNodeWidth,
height: intersectionNodeHeight,
positionAbsolute: intersectionNodePosition,
} = intersectionNode;
const targetPosition = targetNode.positionAbsolute!;
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured;
const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
const targetPosition = targetNode.internals.positionAbsolute!;
const w = intersectionNodeWidth! / 2;
const h = intersectionNodeHeight! / 2;
const x2 = intersectionNodePosition!.x + w;
const y2 = intersectionNodePosition!.y + h;
const x2 = intersectionNodePosition.x + w;
const y2 = intersectionNodePosition.y + h;
const x1 = targetPosition.x + w;
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
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
const n = { ...node.positionAbsolute, ...node };
function getEdgePosition(node: InternalNode, intersectionPoint: XYPosition) {
const n = { ...node.internals.positionAbsolute, ...node };
const nx = Math.round(n.x!);
const ny = Math.round(n.y!);
const px = Math.round(intersectionPoint.x);
@@ -41,13 +39,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) {
return Position.Left;
}
if (px >= nx + n.width! - 1) {
if (px >= nx + n.measured?.width! - 1) {
return Position.Right;
}
if (py <= ny + 1) {
return Position.Top;
}
if (py >= n.y! + n.height! - 1) {
if (py >= n.y! + n.measured?.height! - 1) {
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
export function getEdgeParams(source: Node, target: Node) {
export function getEdgeParams(source: InternalNode, target: InternalNode) {
const sourceIntersectionPoint = getNodeIntersection(source, target);
const targetIntersectionPoint = getNodeIntersection(target, source);
@@ -175,6 +175,15 @@ const edgeTypes: EdgeTypes = {
custom2: CustomEdge2,
};
const defaultEdgeOptions = {
markerEnd: {
type: MarkerType.ArrowClosed,
color: 'red',
width: 20,
height: 20,
},
};
const EdgesFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -196,6 +205,8 @@ const EdgesFlow = () => {
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onDelete={console.log}
defaultEdgeOptions={defaultEdgeOptions}
>
<MiniMap />
<Controls />
+2 -2
View File
@@ -26,8 +26,8 @@ const buttonStyle: CSSProperties = {
};
const EmptyFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
const addRandomNode = () => {
@@ -5,8 +5,8 @@ import { getEdgeParams } from './utils';
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
const { sourceNode, targetNode } = useStore((s) => {
const sourceNode = s.nodes.find((n) => n.id === source);
const targetNode = s.nodes.find((n) => n.id === target);
const sourceNode = s.nodeLookup.get(source);
const targetNode = s.nodeLookup.get(target);
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
// of the line between the center of the intersectionNode and the target node
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
const {
width: intersectionNodeWidth,
height: intersectionNodeHeight,
position: intersectionNodePosition,
} = intersectionNode;
const { position: intersectionNodePosition } = intersectionNode;
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured ?? {
width: 0,
height: 0,
};
const targetPosition = targetNode.position;
const w = (intersectionNodeWidth ?? 0) / 2;
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) {
return Position.Left;
}
if (px >= nx + (n.width ?? 0) - 1) {
if (px >= nx + (n.measured?.width ?? 0) - 1) {
return Position.Right;
}
if (py <= ny + 1) {
return Position.Top;
}
if (py >= n.y + (n.height ?? 0) - 1) {
if (py >= n.y + (n.measured?.height ?? 0) - 1) {
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
export function getEdgeParams(source: Node, target: Node) {
export function getEdgeParams(source: InternalNode, target: InternalNode) {
const sourceIntersectionPoint = getNodeIntersection(source, target);
const targetIntersectionPoint = getNodeIntersection(target, source);
+8 -2
View File
@@ -11,6 +11,7 @@ import {
MiniMap,
Controls,
} from '@xyflow/react';
import ReactFlowDevTools from '../DevTools/DevTools';
const initialNodes: Node[] = [
{
@@ -48,8 +49,10 @@ const initialEdges: Edge[] = [
const setHidden = (hidden: boolean) => (els: any[]) =>
els.map((e: any) => {
e.hidden = hidden;
return e;
return {
...e,
hidden,
};
});
const HiddenFlow = () => {
@@ -70,6 +73,8 @@ const HiddenFlow = () => {
setEdges(setHidden(isHidden));
}, [isHidden, setEdges, setNodes]);
console.log(nodes);
return (
<ReactFlow
nodes={nodes}
@@ -80,6 +85,7 @@ const HiddenFlow = () => {
>
<MiniMap />
<Controls />
<ReactFlowDevTools position="top-right" />
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
<div>
@@ -36,7 +36,7 @@ const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', nod
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', 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 InteractionFlow = () => {
@@ -80,6 +80,7 @@ const InteractionFlow = () => {
onPaneClick={captureZoomClick ? onPaneClick : undefined}
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
nodeDragThreshold={0}
onMoveEnd={onMoveEnd}
>
<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 initialNodes: Node[] = [
{
id: '0',
data: { label: 'rectangle' },
position: { x: 0, y: 0 },
width: 100,
height: 100,
draggable: false,
style: {
opacity: 0.5,
},
},
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
className: 'light',
style: {
width: 200,
height: 100,
},
width: 200,
height: 100,
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 0, y: 150 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 250, y: 0 },
className: 'light',
},
{
id: '4',
@@ -48,7 +54,6 @@ const initialNodes: Node[] = [
width: 50,
height: 50,
},
className: 'light',
},
];
@@ -19,6 +19,7 @@ import {
import initialItems from './initial-elements';
import styles from './layouting.module.css';
import ReactFlowDevTools from '../DevTools/DevTools';
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
@@ -56,16 +57,16 @@ const LayoutFlow = () => {
const layoutedNodes = nodes.map((node) => {
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);
@@ -98,6 +99,7 @@ const LayoutFlow = () => {
onEdgesChange={onEdgesChange}
>
<Controls />
<ReactFlowDevTools />
</ReactFlow>
<Panel position="top-right">
<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}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
isVisible={data.isVisible ?? selected}
isVisible={data.isVisible ?? !!selected}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
@@ -16,8 +16,6 @@ import CustomResizer from './CustomResizer';
import VerticalResizer from './VerticalResizer';
import HorizontalResizer from './HorizontalResizer';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
defaultResizer: DefaultResizer,
customResizer: CustomResizer,
@@ -39,6 +37,7 @@ const initialNodes: Node[] = [
type: 'defaultResizer',
data: { label: 'default resizer' },
position: { x: 0, y: 0 },
origin: [1, 1],
style: { ...nodeStyle },
},
{
@@ -52,7 +51,9 @@ const initialNodes: Node[] = [
maxHeight: 200,
},
position: { x: 0, y: 60 },
style: { ...nodeStyle, width: 100, height: 80 },
width: 100,
height: 80,
style: { ...nodeStyle },
},
{
id: '1b',
@@ -66,9 +67,9 @@ const initialNodes: Node[] = [
maxHeight: 400,
},
position: { x: 250, y: 0 },
width: 174,
height: 123,
style: {
width: 174,
height: 123,
...nodeStyle,
},
},
@@ -77,7 +78,9 @@ const initialNodes: Node[] = [
type: 'customResizer',
data: { label: 'custom resize icon' },
position: { x: 0, y: 200 },
style: { width: 100, height: 60, ...nodeStyle },
width: 100,
height: 60,
style: { ...nodeStyle },
},
{
id: '3',
@@ -96,7 +99,8 @@ const initialNodes: Node[] = [
keepAspectRatio: true,
},
position: { x: 400, y: 200 },
style: { ...nodeStyle, height: 50 },
height: 50,
style: { ...nodeStyle },
},
{
id: '4',
@@ -118,6 +122,50 @@ const initialNodes: Node[] = [
position: { x: 250, y: 400 },
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 = () => {
@@ -142,6 +190,7 @@ const CustomNodeFlow = () => {
maxZoom={5}
snapToGrid={snapToGrid}
fitView
onlyRenderVisibleElements
>
<Controls />
<Panel position="bottom-right">
@@ -28,6 +28,14 @@ const initialNodes: Node[] = [
data: { label: 'A Node' },
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 = {
@@ -66,7 +74,7 @@ const nodeTypesObjects: NodeTypesObject = {
const NodeTypeChangeFlow = () => {
const [nodeTypesId, setNodeTypesId] = useState<string>('a');
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 changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
@@ -78,6 +86,7 @@ const NodeTypeChangeFlow = () => {
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypesObjects[nodeTypesId]}
fitView
>
<button onClick={changeType} style={buttonStyle}>
change type
+14 -8
View File
@@ -15,6 +15,8 @@ import {
Background,
MiniMap,
ConnectionMode,
OnBeforeDelete,
OnDelete,
} from '@xyflow/react';
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 onPaneClick = (event: ReactMouseEvent) => console.log('pane click', 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 onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', 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 onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge);
const onNodesDelete = (nodes: Node[]) => console.log('nodes delete', nodes);
const onEdgesDelete = (edges: Edge[]) => console.log('edges delete', edges);
const onBeforeDelete: OnBeforeDelete = async ({ nodes, 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 initialNodes: Node[] = [
@@ -226,13 +232,13 @@ const OverviewFlow = () => {
fitViewOptions={{ padding: 0.1 /*nodes: [{ id: '1' }]*/ }}
attributionPosition="top-right"
maxZoom={Infinity}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
onBeforeDelete={onBeforeDelete}
onDelete={onDelete}
onPaneMouseMove={onPaneMouseMove}
>
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
<Controls />
<Background color="#aaa" gap={25} />
<MiniMap nodeBorderRadius={2} />
<Controls orientation="horizontal" />
<Background gap={25} />
</ReactFlow>
);
};
@@ -2,11 +2,10 @@ import { useState, useCallback, MouseEvent as ReactMouseEvent } from 'react';
import {
ReactFlow,
Controls,
updateEdge,
reconnectEdge,
addEdge,
applyNodeChanges,
applyEdgeChanges,
ReactFlowInstance,
Connection,
Edge,
Node,
@@ -92,26 +91,24 @@ const initialNodes: Node[] = [
];
const initialEdges: Edge[] = [
{ id: 'e1-3', source: '1', target: '3', label: 'This edge can only be updated from source', updatable: 'source' },
{ id: 'e2-4', source: '2', target: '4', label: 'This edge can only be updated from target', updatable: 'target' },
{ 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', reconnectable: 'target' },
{ id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' },
];
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
const onReconnectStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
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);
const UpdatableEdge = () => {
const ReconnectEdge = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els));
const onReconnect = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
const onNodesChange = useCallback((changes: NodeChange[]) => {
console.log(changes);
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
@@ -125,16 +122,16 @@ const UpdatableEdge = () => {
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onInit={onInit}
snapToGrid={true}
onEdgeUpdate={onEdgeUpdate}
onReconnect={onReconnect}
onConnect={onConnect}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
fitView
>
<Controls />
</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 {
ReactFlow,
ReactFlowInstance,
Edge,
Node,
NodeChange,
@@ -12,16 +11,11 @@ import {
EdgeChange,
Controls,
Background,
MiniMap,
Panel,
} from '@xyflow/react';
import { getNodesAndEdges } from './utils';
const onInit = (reactFlowInstance: ReactFlowInstance) => {
reactFlowInstance.fitView();
console.log(reactFlowInstance.getNodes());
};
import { FrameRecorder, generateMouseEventParamsTargetingNode, nextFrame } from './performanceUtils';
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
@@ -31,6 +25,166 @@ const StressFlow = () => {
const onConnect = useCallback((connection: Connection) => {
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 = () => {
setNodes((nds) => {
return nds.map((n) => {
@@ -62,19 +216,23 @@ const StressFlow = () => {
return (
<ReactFlow
key={key}
nodes={nodes}
edges={edges}
onInit={onInit}
onConnect={onConnect}
onNodesChange={onNodesChange}
onEdgesChange={onEdgeChange}
minZoom={0.2}
fitView
>
<MiniMap />
<Controls />
<Background />
<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={updateElements}>update elements</button>
</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 },
data,
position,
width: 50,
height: 30,
};
initialNodes.push(node);
@@ -1,6 +1,6 @@
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 idStyle: CSSProperties = {
@@ -11,13 +11,18 @@ const idStyle: CSSProperties = {
left: 2,
};
const DebugNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => {
const node = useInternalNode(id)!;
return (
<>
<Handle type="target" position={Position.Top} />
<div style={idStyle}>{id}</div>
<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>
<Handle type="source" position={Position.Bottom} />
</>
+40 -29
View File
@@ -13,6 +13,9 @@ import {
MiniMap,
Background,
Panel,
NodeOrigin,
useUpdateNodeInternals,
ReactFlowProvider,
} from '@xyflow/react';
import DebugNode from './DebugNode';
@@ -49,7 +52,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 4a' },
position: { x: 15, y: 15 },
className: 'light',
parentNode: '4',
parentId: '4',
origin: [0.5, 0.5],
extent: [
@@ -67,21 +70,21 @@ const initialNodes: Node[] = [
height: 200,
width: 300,
},
parentNode: '4',
parentId: '4',
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 40, y: 20 },
className: 'light',
parentNode: '4b',
parentId: '4b',
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 20, y: 100 },
className: 'light',
parentNode: '4b',
parentId: '4b',
},
{
id: '5',
@@ -89,7 +92,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 5' },
position: { x: 650, y: 250 },
className: 'light',
style: { width: 400, height: 150 },
style: { width: 100, height: 100 },
zIndex: 1000,
},
{
@@ -97,15 +100,15 @@ const initialNodes: Node[] = [
data: { label: 'Node 5a' },
position: { x: 0, y: 0 },
className: 'light',
parentNode: '5',
parentId: '5',
extent: 'parent',
},
{
id: '5b',
data: { label: 'Node 5b' },
position: { x: 225, y: 50 },
position: { x: 200, y: 200 },
className: 'light',
parentNode: '5',
parentId: '5',
expandParent: true,
},
{
@@ -119,6 +122,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
extent: 'parent',
},
];
@@ -149,6 +153,7 @@ const nodeTypes = {
const Subflow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const updateNodeInternals = useUpdateNodeInternals();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -158,10 +163,13 @@ const Subflow = () => {
const updatePos = () => {
setNodes((nds) => {
return nds.map((n) => {
if (!n.parentNode) {
n.position = {
x: Math.random() * 400,
y: Math.random() * 400,
if (!n.parentId) {
return {
...n,
position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
};
}
@@ -176,8 +184,10 @@ const Subflow = () => {
const toggleClassnames = () => {
setNodes((nds) => {
return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light';
return n;
return {
...n,
className: n.className === 'light' ? 'dark' : 'light',
};
});
});
};
@@ -185,8 +195,10 @@ const Subflow = () => {
const toggleChildNodes = () => {
setNodes((nds) => {
return nds.map((n) => {
n.hidden = !!n.parentNode && !n.hidden;
return n;
return {
...n,
hidden: !!n.parentId && !n.hidden,
};
});
});
};
@@ -207,28 +219,27 @@ const Subflow = () => {
onlyRenderVisibleElements={false}
nodeTypes={nodeTypes}
fitView
nodeOrigin={[0, 0]}
>
<MiniMap />
<Controls />
<Background />
<Panel position="top-right">
<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={resetTransform}>reset transform</button>
<button onClick={updatePos}>change pos</button>
<button onClick={toggleClassnames}>toggle classnames</button>
<button onClick={toggleChildNodes}>toggleChildNodes</button>
<button onClick={logToObject}>toObject</button>
<button onClick={() => setNodes(initialNodes)}>setNodes</button>
<button onClick={() => updateNodeInternals(nodes.map((node) => node.id))}>updateNodeInternals</button>
</Panel>
</ReactFlow>
);
};
export default Subflow;
export default () => (
<ReactFlowProvider>
<Subflow />
</ReactFlowProvider>
);
@@ -1,7 +1,7 @@
import { useCallback } from '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[] = [
{
@@ -42,7 +42,7 @@ const TouchDeviceFlow = () => {
onConnectEnd={onConnectEnd}
onClickConnectStart={onClickConnectStart}
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,
ConnectionLineType,
ConnectionMode,
updateEdge,
reconnectEdge,
useNodesState,
useEdgesState,
} from '@xyflow/react';
@@ -22,55 +22,55 @@ const initialNodes: Node[] = [
id: '00',
type: 'custom',
position: { x: 300, y: 250 },
data: null,
data: {},
},
{
id: '01',
type: 'custom',
position: { x: 100, y: 50 },
data: null,
data: {},
},
{
id: '02',
type: 'custom',
position: { x: 500, y: 50 },
data: null,
data: {},
},
{
id: '03',
type: 'custom',
position: { x: 500, y: 500 },
data: null,
data: {},
},
{
id: '04',
type: 'custom',
position: { x: 100, y: 500 },
data: null,
data: {},
},
{
id: '10',
type: 'custom',
position: { x: 300, y: 5 },
data: null,
data: {},
},
{
id: '20',
type: 'custom',
position: { x: 600, y: 250 },
data: null,
data: {},
},
{
id: '30',
type: 'custom',
position: { x: 300, y: 600 },
data: null,
data: {},
},
{
id: '40',
type: 'custom',
position: { x: 5, y: 250 },
data: null,
data: {},
},
];
@@ -183,11 +183,11 @@ const getId = () => `${id++}`;
const UpdateNodeInternalsFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
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 onEdgeUpdate = useCallback(
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)),
const onReconnect = useCallback(
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),
[]
);
@@ -196,12 +196,12 @@ const UpdateNodeInternalsFlow = () => {
setNodes((nds) =>
nds.concat({
id: getId(),
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
position: screenToFlowPosition({ x: evt.clientX, y: evt.clientY }),
type: 'custom',
data: null,
})
),
[project, setNodes]
[screenToFlowPosition, setNodes]
);
return (
@@ -215,7 +215,7 @@ const UpdateNodeInternalsFlow = () => {
onPaneClick={onPaneClick}
connectionLineType={ConnectionLineType.Bezier}
connectionMode={ConnectionMode.Loose}
onEdgeUpdate={onEdgeUpdate}
onReconnect={onReconnect}
/>
);
};
@@ -1,5 +1,5 @@
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';
@@ -13,6 +13,7 @@ const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const UpdateNode = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const { updateNode } = useReactFlow();
const [nodeName, setNodeName] = useState<string>('Node 1');
const [nodeBg, setNodeBg] = useState<string>('#eee');
@@ -23,9 +24,12 @@ const UpdateNode = () => {
nds.map((n) => {
if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change
n.data = {
...n.data,
label: nodeName,
return {
...n,
data: {
...n.data,
label: nodeName,
},
};
}
@@ -39,7 +43,10 @@ const UpdateNode = () => {
nds.map((n) => {
if (n.id === '1') {
// 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;
@@ -51,8 +58,10 @@ const UpdateNode = () => {
setNodes((nds) =>
nds.map((n) => {
if (n.id === '1' || n.id === 'e1-2') {
// when you update a simple type you can just update the value
n.hidden = nodeHidden;
return {
...n,
hidden: nodeHidden,
};
}
return n;
@@ -80,9 +89,19 @@ const UpdateNode = () => {
<label>hidden:</label>
<input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} />
</div>
<button
onClick={() => updateNode('1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }))}
>
update position
</button>
</div>
</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 { useEffect } from 'react';
const UseKeyPressComponent = () => {
const metaPressed = useKeyPress(['Meta']);
console.log({ metaPressed });
useEffect(() => {
console.log({ metaPressed });
}, [metaPressed]);
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,
OnConnect,
useNodesInitialized,
Panel,
useReactFlow,
} from '@xyflow/react';
const initialNodes: Node[] = [
@@ -49,6 +51,7 @@ const initialEdges: Edge[] = [
];
const UseZoomPanHelperFlow = () => {
const { addNodes } = useReactFlow();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -59,6 +62,13 @@ const UseZoomPanHelperFlow = () => {
console.log('initialized', initialized);
}, [initialized]);
const addNode = () =>
addNodes({
id: `${Math.random()}`,
data: { label: 'new node' },
position: { x: Math.random() * 400, y: Math.random() * 400 },
});
return (
<ReactFlow
nodes={nodes}
@@ -71,6 +81,9 @@ const UseZoomPanHelperFlow = () => {
>
<Background />
<MiniMap />
<Panel>
<button onClick={addNode}>add node</button>
</Panel>
</ReactFlow>
);
};
@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import {
ReactFlow,
addEdge,
@@ -10,6 +10,7 @@ import {
useEdgesState,
useOnSelectionChange,
OnSelectionChangeParams,
OnSelectionChangeFunc,
} from '@xyflow/react';
const initialNodes: Node[] = [
@@ -35,9 +36,9 @@ const initialEdges: Edge[] = [
},
];
const SelectionLogger = () => {
const SelectionLogger = ({ id }: { id: string }) => {
const onChange = useCallback(({ nodes, edges }: OnSelectionChangeParams) => {
console.log(nodes, edges);
console.log(id, nodes, edges);
}, []);
useOnSelectionChange({
@@ -63,14 +64,23 @@ const Flow = () => {
);
};
const WrappedFlow = () => (
<ReactFlowProvider>
<Flow />
<SelectionLogger />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<input type={'text'} placeholder={'name'} />
</div>
</ReactFlowProvider>
);
const WrappedFlow = () => {
const [secondLoggerActive, setSecondLoggerActive] = useState<boolean>(true);
const toggleSecondLogger = () => {
setSecondLoggerActive(!secondLoggerActive);
};
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;
@@ -1,4 +1,4 @@
import { useCallback, MouseEvent, useEffect } from 'react';
import { useCallback, MouseEvent, useEffect, useRef } from 'react';
import {
ReactFlow,
Background,
@@ -27,18 +27,21 @@ const initialNodes: Node[] = [
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
type: 'default',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
type: 'default',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
type: 'default',
},
];
@@ -57,7 +60,7 @@ const UseZoomPanHelperFlow = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const {
project,
screenToFlowPosition,
setCenter,
zoomIn,
zoomOut,
@@ -68,13 +71,14 @@ const UseZoomPanHelperFlow = () => {
getNodes,
getEdges,
deleteElements,
updateNodeData,
} = useReactFlow();
const onPaneClick = useCallback(
(evt: MouseEvent) => {
const projectedPosition = project({
const projectedPosition = screenToFlowPosition({
x: evt.clientX,
y: evt.clientY - 40,
y: evt.clientY,
});
setNodes((nds) =>
@@ -84,10 +88,11 @@ const UseZoomPanHelperFlow = () => {
data: {
label: `${projectedPosition.x}-${projectedPosition.y}`,
},
type: 'default',
})
);
},
[project, setNodes]
[screenToFlowPosition, setNodes]
);
const onNodeClick = useCallback(
@@ -105,6 +110,7 @@ const UseZoomPanHelperFlow = () => {
data: {
label: 'New Node',
},
type: 'default',
};
addNodes(newNode);
@@ -125,12 +131,31 @@ const UseZoomPanHelperFlow = () => {
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
}, []);
const edgeAdded = useRef(false);
useEffect(() => {
addEdges({ id: 'e3-4', source: '3', target: '4' });
if (!edgeAdded.current) {
addEdges({ id: 'e3-4', source: '3', target: '4' });
edgeAdded.current = true;
}
}, [addEdges]);
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 (
<ReactFlow
nodes={nodes}
@@ -153,6 +178,8 @@ const UseZoomPanHelperFlow = () => {
<button onClick={logNodes}>useNodes</button>
<button onClick={deleteSelectedElements}>deleteSelectedElements</button>
<button onClick={deleteSomeElements}>deleteSomeElements</button>
<button onClick={onSetNodes}>setNodes</button>
<button onClick={onUpdateNode}>updateNode</button>
</Panel>
<Background />
<MiniMap />
@@ -33,23 +33,23 @@ const getId = (): string => `${id++}`;
const UpdateNodeInternalsFlow = () => {
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 { project } = useReactFlow();
const { screenToFlowPosition } = useReactFlow();
const onPaneClick = useCallback(
(evt: MouseEvent) =>
setNodes((nds) =>
nds.concat({
id: getId(),
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
position: screenToFlowPosition({ x: evt.clientX, y: evt.clientY }),
data: { label: 'new node' },
targetPosition: Position.Left,
sourcePosition: Position.Right,
})
),
[project, setNodes]
[screenToFlowPosition, setNodes]
);
return (

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