@dschz/solid-flow 0.3.0-next.3 → 0.3.0-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +239 -209
- package/dist/index/index.d.ts +164 -59
- package/dist/index/index.js +238 -116
- package/dist/index/index.jsx +170 -73
- package/dist/styles/index.css +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,100 +1,69 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
|
|
2
|
+
<!-- the %20%20 suffix works around a banner-generator bug that clips a trailing W -->
|
|
3
|
+
<img src="https://assets.solidjs.com/banner?project=solid-flow%20%20&type=Ecosystem&background=tiles" alt="@dschz/solid-flow banner" />
|
|
3
4
|
</p>
|
|
4
5
|
|
|
5
6
|
# Solid Flow
|
|
6
7
|
|
|
7
8
|
[](LICENSE)
|
|
8
9
|
[](https://www.npmjs.com/package/@dschz/solid-flow)
|
|
10
|
+
[](https://jsr.io/@dschz/solid-flow)
|
|
9
11
|
[](https://bundlephobia.com/package/@dschz/solid-flow)
|
|
10
12
|
[](https://github.com/dsnchz/solid-flow/actions/workflows/ci.yaml)
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
A SolidJS port of [React Flow](https://reactflow.dev/) and [Svelte Flow](https://svelteflow.dev/): build node-based editors, diagrams, and interactive graphs with the same battle-tested gesture system ([@xyflow/system](https://github.com/xyflow/xyflow)) and an API designed around Solid's fine-grained reactivity.
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
## Version pairing
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
| Solid Flow | SolidJS | Status |
|
|
19
|
+
| ---------- | --------------- | ------------------- |
|
|
20
|
+
| `0.3.x` | `solid-js` 2.x | Active development |
|
|
21
|
+
| `0.2.x` | `solid-js` 1.9+ | Maintenance (fixes) |
|
|
17
22
|
|
|
18
|
-
-
|
|
19
|
-
- [Custom MiniMap Nodes](https://github.com/dsnchz/solid-flow/issues/12): define custom minimap jsx node elements.
|
|
20
|
-
- [Edge Reconnect Anchors](https://github.com/dsnchz/solid-flow/issues/13): the ability to re-connect already connected edges.
|
|
23
|
+
The 0.3 line is built for SolidJS 2.0 and its deferred, fine-grained reactive graph. Keep `solid-js` and `@solidjs/web` on matching 2.0 versions — mixing them breaks at import time. Upgrading from 0.2.x? See [Migrating from 0.2.x](#migrating-from-02x).
|
|
21
24
|
|
|
22
25
|
## Key Features
|
|
23
26
|
|
|
24
|
-
- **Easy to use:**
|
|
25
|
-
- **Customizable:**
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **TypeScript First:** Fully typed API with generic type support and IntelliSense integration
|
|
27
|
+
- **Easy to use:** Zooming and panning, single- and multi-selection of graph elements, and keyboard shortcuts out of the box
|
|
28
|
+
- **Customizable:** Built-in node types (Input, Output, Default, Group) and edge types (Bezier, Straight, Step, SmoothStep) with full support for custom nodes and edges — including edge reconnection via `EdgeReconnectAnchor`
|
|
29
|
+
- **Fine-grained by construction:** A node drag is a handful of DOM attribute writes — no virtual DOM, no component re-renders, no memoization ceremony
|
|
30
|
+
- **Scales to large graphs:** Off-screen elements are viewport-culled by default, and an opt-in `onlyRenderVisibleElements` mode only mounts what's visible — stress-tested at 10,000 nodes
|
|
31
|
+
- **Type-guided data:** Your custom components are the schema — node/edge stores narrow each element's `data` by its `type` field, with autocomplete for type names
|
|
32
|
+
- **Rich plugins:** Background patterns, interactive MiniMap (custom minimap nodes, click handlers), zoom Controls, Node Toolbar, and Node Resizer
|
|
33
|
+
- **Accessible:** Keyboard navigation, screen reader support, ARIA labels, and focus management
|
|
34
|
+
- **SSR-ready:** Renders on the server (a dedicated SSR test lane keeps it that way)
|
|
33
35
|
|
|
34
36
|
## Installation
|
|
35
37
|
|
|
36
|
-
The easiest way to get the latest version of Solid Flow is to install it via npm, yarn or pnpm:
|
|
37
|
-
|
|
38
38
|
```sh
|
|
39
|
-
npm
|
|
40
|
-
pnpm
|
|
41
|
-
yarn
|
|
42
|
-
bun
|
|
39
|
+
npm install @dschz/solid-flow
|
|
40
|
+
pnpm add @dschz/solid-flow
|
|
41
|
+
yarn add @dschz/solid-flow
|
|
42
|
+
bun add @dschz/solid-flow
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
`solid-js` is a peer dependency. Also available on [JSR](https://jsr.io/@dschz/solid-flow).
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
## Quick Start
|
|
48
48
|
|
|
49
49
|
```tsx
|
|
50
50
|
import {
|
|
51
|
-
SolidFlow,
|
|
52
|
-
SolidFlowProvider,
|
|
53
|
-
Controls,
|
|
54
51
|
Background,
|
|
55
|
-
|
|
56
|
-
addEdge,
|
|
57
|
-
type EdgeConnection,
|
|
52
|
+
Controls,
|
|
58
53
|
createEdgeStore,
|
|
59
54
|
createNodeStore,
|
|
60
|
-
type
|
|
55
|
+
type EdgeConnection,
|
|
56
|
+
MiniMap,
|
|
57
|
+
Panel,
|
|
58
|
+
SolidFlow,
|
|
61
59
|
} from "@dschz/solid-flow";
|
|
62
|
-
import "@dschz/solid-flow/styles"; //
|
|
63
|
-
|
|
64
|
-
import { createStore, produce } from "solid-js/store";
|
|
65
|
-
|
|
66
|
-
export default function Page() {
|
|
67
|
-
return (
|
|
68
|
-
<SolidFlowProvider>
|
|
69
|
-
<Flow />
|
|
70
|
-
</SolidFlowProvider>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
60
|
+
import "@dschz/solid-flow/styles"; // required styles, import once
|
|
73
61
|
|
|
74
|
-
|
|
75
|
-
// Can invoke useSolidFlow due to parent Page + SolidFlowProvider wrapper. Contains all helper APIs
|
|
76
|
-
const { .. } = useSolidFlow();
|
|
77
|
-
|
|
78
|
-
// Use createNodeStore and createEdgeStore for reactive state management
|
|
62
|
+
export const Flow = () => {
|
|
79
63
|
const [nodes, setNodes] = createNodeStore([
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
data: { label: "Input Node" },
|
|
84
|
-
position: { x: 250, y: 0 },
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: "2",
|
|
88
|
-
type: "default",
|
|
89
|
-
data: { label: "Default Node" },
|
|
90
|
-
position: { x: 100, y: 100 },
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
id: "3",
|
|
94
|
-
type: "output",
|
|
95
|
-
data: { label: "Output Node" },
|
|
96
|
-
position: { x: 250, y: 200 },
|
|
97
|
-
},
|
|
64
|
+
{ id: "1", type: "input", data: { label: "Input" }, position: { x: 250, y: 0 } },
|
|
65
|
+
{ id: "2", type: "default", data: { label: "Default" }, position: { x: 100, y: 100 } },
|
|
66
|
+
{ id: "3", type: "output", data: { label: "Output" }, position: { x: 250, y: 200 } },
|
|
98
67
|
]);
|
|
99
68
|
|
|
100
69
|
const [edges, setEdges] = createEdgeStore([
|
|
@@ -102,216 +71,269 @@ function Flow() {
|
|
|
102
71
|
{ id: "e2-3", source: "2", target: "3" },
|
|
103
72
|
]);
|
|
104
73
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const updateViewport = () => {
|
|
112
|
-
setViewport("x", (prev) => prev + 10);
|
|
74
|
+
// Your stores own which elements exist — adopt new connections into
|
|
75
|
+
// your edge store (see "Who owns the data" below).
|
|
76
|
+
const onConnect = (connection: EdgeConnection) => {
|
|
77
|
+
setEdges((edges) => {
|
|
78
|
+
edges.push(connection);
|
|
79
|
+
});
|
|
113
80
|
};
|
|
114
81
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
(edge) => edge.id === connection.id,
|
|
121
|
-
produce((edge) => {
|
|
122
|
-
edge.animated = true;
|
|
123
|
-
}),
|
|
124
|
-
);
|
|
82
|
+
const nudge = () => {
|
|
83
|
+
// SolidJS 2.0 store writes are drafts: mutate what changed.
|
|
84
|
+
setNodes((nodes) => {
|
|
85
|
+
nodes[0]!.position.x += 20;
|
|
86
|
+
});
|
|
125
87
|
};
|
|
126
88
|
|
|
127
89
|
return (
|
|
128
90
|
<SolidFlow nodes={nodes} edges={edges} onConnect={onConnect} fitView>
|
|
129
91
|
<Controls />
|
|
130
92
|
<MiniMap />
|
|
93
|
+
<Background variant="dots" />
|
|
131
94
|
<Panel position="top-left">
|
|
132
|
-
<button onClick={
|
|
95
|
+
<button onClick={nudge}>Nudge first node</button>
|
|
133
96
|
</Panel>
|
|
134
|
-
<Background variant="dots" />
|
|
135
97
|
</SolidFlow>
|
|
136
98
|
);
|
|
137
|
-
}
|
|
99
|
+
};
|
|
138
100
|
```
|
|
139
101
|
|
|
140
|
-
##
|
|
102
|
+
## Your components are the schema
|
|
141
103
|
|
|
142
|
-
|
|
104
|
+
`createNodeStore` / `createEdgeStore` derive each element's `data` type from the component registered for its `type` — the renderer map is the single source of truth, and the stores narrow against it:
|
|
143
105
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
106
|
+
```tsx
|
|
107
|
+
import {
|
|
108
|
+
createNodeStore,
|
|
109
|
+
Handle,
|
|
110
|
+
type NodeProps,
|
|
111
|
+
type NodeTypes,
|
|
112
|
+
Position,
|
|
113
|
+
} from "@dschz/solid-flow";
|
|
150
114
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
115
|
+
const CounterNode = (props: NodeProps<{ count: number }, "counter">) => (
|
|
116
|
+
<div style={{ padding: "10px", background: "white", border: "1px solid #333" }}>
|
|
117
|
+
<Handle type="target" position={Position.Top} />
|
|
118
|
+
<div>count: {props.data.count}</div>
|
|
119
|
+
<Handle type="source" position={Position.Bottom} />
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
155
122
|
|
|
156
|
-
|
|
123
|
+
const nodeTypes = { counter: CounterNode } satisfies NodeTypes;
|
|
157
124
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
125
|
+
const [nodes] = createNodeStore<typeof nodeTypes>([
|
|
126
|
+
// "counter" narrows data to { count: number }; built-ins still work
|
|
127
|
+
{ id: "a", type: "counter", data: { count: 1 }, position: { x: 0, y: 0 } },
|
|
128
|
+
{ id: "b", type: "default", data: { label: "hi" }, position: { x: 0, y: 100 } },
|
|
129
|
+
// { id: "c", type: "countr", ... } <-- compile error: unknown type name
|
|
130
|
+
]);
|
|
163
131
|
|
|
164
|
-
|
|
132
|
+
<SolidFlow nodes={nodes} nodeTypes={nodeTypes} />;
|
|
133
|
+
```
|
|
165
134
|
|
|
166
|
-
|
|
135
|
+
The same guided unions are exported as standalone types, so plain arrays, props, and vanilla stores get identical narrowing:
|
|
167
136
|
|
|
168
137
|
```tsx
|
|
169
|
-
|
|
170
|
-
const solidFlow = useSolidFlow();
|
|
138
|
+
import type { EdgesFor, NodesFor } from "@dschz/solid-flow";
|
|
171
139
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
140
|
+
const initialNodes = [
|
|
141
|
+
{ id: "a", type: "counter", data: { count: 1 }, position: { x: 0, y: 0 } },
|
|
142
|
+
] satisfies NodesFor<typeof nodeTypes>[];
|
|
143
|
+
```
|
|
175
144
|
|
|
176
|
-
|
|
177
|
-
const viewport = useViewport();
|
|
145
|
+
## Who owns the data
|
|
178
146
|
|
|
179
|
-
|
|
180
|
-
const connection = useConnection();
|
|
147
|
+
The stores you pass as `nodes` / `edges` props are **controlled** — a deliberate contract:
|
|
181
148
|
|
|
182
|
-
|
|
183
|
-
|
|
149
|
+
- **Your store owns membership.** Which nodes and edges exist is decided by your store. Any write form works: draft mutations update in place (`O(changed)`), and wholesale replacement (`setNodes(() => next)`) re-seeds the flow — rows are keyed by `id`, so surviving elements keep their runtime state.
|
|
150
|
+
- **The flow writes runtime fields onto your rows.** Dragging updates `position`, selection updates `selected`, measurement fills `measured` — on the same objects you provided, so reading your store is always live.
|
|
151
|
+
- **Imperative commands don't write membership back.** `commands.addNodes(...)` and friends update the flow, not your store. To keep an element across a store replacement, adopt it — like the `onConnect` handler in the Quick Start pushing the new connection into the edge store.
|
|
184
152
|
|
|
185
|
-
|
|
186
|
-
const connections = useNodeConnections("node-1");
|
|
187
|
-
```
|
|
153
|
+
## The flow API
|
|
188
154
|
|
|
189
|
-
|
|
155
|
+
`useSolidFlow()` returns `{ flow, commands }` (with `commands` also spread at the top level for React/Svelte Flow familiarity). Both are stable identities — destructuring is safe.
|
|
190
156
|
|
|
191
157
|
```tsx
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
158
|
+
import { SolidFlowProvider, useSolidFlow } from "@dschz/solid-flow";
|
|
159
|
+
|
|
160
|
+
const Toolbar = () => {
|
|
161
|
+
const { flow, commands } = useSolidFlow();
|
|
162
|
+
|
|
163
|
+
// flow.* reads are reactive: use them in JSX or tracked scopes.
|
|
164
|
+
// flow.nodes, flow.edges, flow.viewport, flow.selection,
|
|
165
|
+
// flow.nodesInitialized, flow.viewportInitialized, ...
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div>
|
|
169
|
+
<span>{flow.selection.nodes.length} selected</span>
|
|
170
|
+
<button onClick={() => commands.fitView()}>Fit</button>
|
|
171
|
+
<button onClick={() => commands.zoomIn()}>+</button>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Hooks used outside <SolidFlow> children need a provider:
|
|
177
|
+
export const App = () => (
|
|
178
|
+
<SolidFlowProvider>
|
|
179
|
+
<Toolbar />
|
|
180
|
+
<Flow />
|
|
181
|
+
</SolidFlowProvider>
|
|
203
182
|
);
|
|
183
|
+
```
|
|
204
184
|
|
|
205
|
-
|
|
206
|
-
setEdges(addEdge(connection, edges));
|
|
185
|
+
Commands include viewport control (`fitView`, `fitBounds`, `zoomIn`/`zoomOut`, `setZoom`, `setCenter`, `setViewport`, `panBy`), element updates (`updateNode`, `updateNodeData`, `updateEdge`, `addNodes`, `addEdges`, `deleteElements`), coordinate conversion (`screenToFlowPosition`, `flowToScreenPosition`), and geometry queries (`isNodeIntersecting`, `getIntersectingNodes`).
|
|
207
186
|
|
|
208
|
-
|
|
209
|
-
const { screenToFlowPosition, flowToScreenPosition } = useSolidFlow();
|
|
187
|
+
## Hooks
|
|
210
188
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
189
|
+
```tsx
|
|
190
|
+
const solidFlow = useSolidFlow(); // { flow, commands } — the main API
|
|
191
|
+
const nodes = useNodes(); // reactive readonly node array
|
|
192
|
+
const edges = useEdges(); // reactive readonly edge array
|
|
193
|
+
const viewport = useViewport(); // reactive viewport accessor
|
|
194
|
+
const connection = useConnection(); // in-progress connection state
|
|
195
|
+
|
|
196
|
+
// Hook parameters that feed reactive reads are accessors, so they can't
|
|
197
|
+
// silently go stale when derived from props:
|
|
198
|
+
const nodeData = useNodesData(() => ["node-1", "node-2"]);
|
|
199
|
+
const connections = useNodeConnections(() => ({ id: "node-1" }));
|
|
200
|
+
const internal = useInternalNode(() => props.nodeId); // measured/internal record
|
|
201
|
+
|
|
202
|
+
const updateInternals = useUpdateNodeInternals(); // re-measure after handle changes
|
|
203
|
+
|
|
204
|
+
// Which node/edge a nested component is rendered inside — for composable
|
|
205
|
+
// custom nodes and edge labels without prop drilling:
|
|
206
|
+
const nodeId = useNodeId();
|
|
207
|
+
const edgeId = useEdgeId();
|
|
214
208
|
```
|
|
215
209
|
|
|
216
|
-
|
|
210
|
+
### Coming from React Flow?
|
|
211
|
+
|
|
212
|
+
Some React Flow hooks intentionally have no Solid Flow equivalent, because Solid's fine-grained reactivity makes them unnecessary:
|
|
217
213
|
|
|
218
|
-
|
|
214
|
+
- `useOnSelectionChange` / `useOnViewportChange` — reading `flow.selection` or `flow.viewport` in a tracked scope IS the subscription; wrap side effects in `createEffect` over those reads.
|
|
215
|
+
- `useNodesState` / `useEdgesState` — use `createNodeStore` / `createEdgeStore` instead.
|
|
216
|
+
- `useStore` — the reactive `flow` struct is the supported read surface; there is no public escape hatch into internals.
|
|
219
217
|
|
|
220
|
-
|
|
218
|
+
## Custom edges and reconnection
|
|
221
219
|
|
|
222
220
|
```tsx
|
|
223
|
-
import {
|
|
221
|
+
import { BaseEdge, EdgeReconnectAnchor, type EdgeProps, getBezierPath } from "@dschz/solid-flow";
|
|
222
|
+
|
|
223
|
+
const ReconnectableEdge = (props: EdgeProps) => {
|
|
224
|
+
const path = () =>
|
|
225
|
+
getBezierPath({
|
|
226
|
+
sourceX: props.sourceX,
|
|
227
|
+
sourceY: props.sourceY,
|
|
228
|
+
sourcePosition: props.sourcePosition,
|
|
229
|
+
targetX: props.targetX,
|
|
230
|
+
targetY: props.targetY,
|
|
231
|
+
targetPosition: props.targetPosition,
|
|
232
|
+
})[0];
|
|
224
233
|
|
|
225
|
-
// Type-safe custom node component
|
|
226
|
-
function CustomNode(props: NodeProps<{ label: string }, "custom">) {
|
|
227
234
|
return (
|
|
228
|
-
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
<
|
|
232
|
-
<
|
|
233
|
-
|
|
235
|
+
<>
|
|
236
|
+
<BaseEdge path={path()} markerEnd={props.markerEnd} />
|
|
237
|
+
{/* drag either end of a selected edge onto another handle */}
|
|
238
|
+
<EdgeReconnectAnchor type="source" position={{ x: props.sourceX, y: props.sourceY }} />
|
|
239
|
+
<EdgeReconnectAnchor type="target" position={{ x: props.targetX, y: props.targetY }} />
|
|
240
|
+
</>
|
|
234
241
|
);
|
|
235
|
-
}
|
|
242
|
+
};
|
|
243
|
+
```
|
|
236
244
|
|
|
237
|
-
|
|
238
|
-
const nodeTypes = {
|
|
239
|
-
custom: CustomNode,
|
|
240
|
-
} satisfies NodeTypes;
|
|
245
|
+
Reconnection lifecycle callbacks (`onReconnectStart`, `onReconnect`, `onReconnectEnd`, `onBeforeReconnect`) are available on `<SolidFlow>`.
|
|
241
246
|
|
|
242
|
-
|
|
243
|
-
const [nodes] = createNodeStore<typeof nodeTypes>([...]);
|
|
247
|
+
## Built-in components
|
|
244
248
|
|
|
245
|
-
|
|
246
|
-
```
|
|
249
|
+
**Node types:** `input` (source handle only), `output` (target handle only), `default` (both), `group` (container for subflows)
|
|
247
250
|
|
|
248
|
-
|
|
251
|
+
**Edge types:** `default` (bezier), `straight`, `step`, `smoothstep`
|
|
249
252
|
|
|
250
|
-
|
|
251
|
-
import { type Connection } from "@dschz/solid-flow";
|
|
253
|
+
**Plugins:**
|
|
252
254
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
- **Background** — dots, lines, and cross patterns
|
|
256
|
+
- **Controls** — zoom in/out, fit view, lock interactions
|
|
257
|
+
- **MiniMap** — interactive overview with viewport indicator, custom node rendering via `nodeComponent`, and `onClick` / `onNodeClick` handlers
|
|
258
|
+
- **NodeToolbar** — context-sensitive toolbars for nodes
|
|
259
|
+
- **NodeResizer** — interactive node resizing with handles
|
|
257
260
|
|
|
258
|
-
|
|
259
|
-
console.log("New connection:", connection);
|
|
260
|
-
setEdges(addEdge(connection, edges));
|
|
261
|
-
};
|
|
261
|
+
## Performance
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
isValidConnection={isValidConnection}
|
|
265
|
-
onConnect={onConnect}
|
|
266
|
-
...
|
|
267
|
-
/>
|
|
268
|
-
```
|
|
263
|
+
Two complementary culling tiers keep large graphs fast:
|
|
269
264
|
|
|
270
|
-
|
|
265
|
+
- **CSS culling (always on):** Elements outside the (overscanned) viewport are hidden with `visibility: hidden` + `pointer-events: none`. Everything stays mounted, so component state, measurement, and accessibility semantics are untouched — this tier has no userland contract at all.
|
|
266
|
+
- **`onlyRenderVisibleElements` (opt-in):** Off-screen elements are **unmounted entirely** and remount as the viewport reaches them. At 10,000 nodes this cuts the DOM ~16x, roughly halves memory, and makes node drags several times faster. Positions, selection, and cached measurements live in the flow's data graph — outside your components — so elements come back exactly as they left. Component-_local_ state does not survive unmounting: keep state you care about in `node.data`. Selected elements, unmeasured nodes, and the node holding focus are never unmounted — and an element whose content must keep running off-screen (media, timers, embeds) can opt out of culling entirely with `cullable: false` on the node or edge.
|
|
271
267
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
onPaneClick={(event) => console.log("Pane clicked")}
|
|
278
|
-
onSelectionChange={(params) => console.log("Selection changed:", params)}
|
|
279
|
-
/>
|
|
280
|
-
```
|
|
268
|
+
The MiniMap always renders the full graph in either mode — it reads the data graph, not the DOM.
|
|
269
|
+
|
|
270
|
+
## Server-side rendering
|
|
271
|
+
|
|
272
|
+
`<SolidFlow>` renders with `renderToString` and hydrates cleanly. For meaningful server-rendered layout, give nodes explicit `width` / `height` (there is no DOM to measure on the server) — the same guidance as React/Svelte Flow.
|
|
281
273
|
|
|
282
274
|
## Accessibility
|
|
283
275
|
|
|
284
|
-
|
|
276
|
+
- Full keyboard navigation (arrow-key node movement, focus traversal, Escape to deselect)
|
|
277
|
+
- Screen reader support with ARIA labels and live announcements
|
|
278
|
+
- Focus management, including under viewport culling
|
|
279
|
+
- Color mode support (`light` / `dark` / `system`)
|
|
285
280
|
|
|
286
|
-
|
|
287
|
-
- Screen reader compatibility with ARIA labels
|
|
288
|
-
- Focus management and visual indicators
|
|
289
|
-
- High contrast and color mode support
|
|
290
|
-
- Customizable keyboard shortcuts
|
|
281
|
+
## Migrating from 0.2.x
|
|
291
282
|
|
|
292
|
-
|
|
283
|
+
The 0.3 line targets SolidJS 2.0, which changes how you write to stores, and reworks the read API. The gestures, components, plugins, and commands are otherwise the same.
|
|
293
284
|
|
|
294
|
-
|
|
295
|
-
- **Viewport Optimization**: Option to render only visible elements (coming soon)
|
|
296
|
-
- **Memory Efficient**: Optimized data structures for large graphs
|
|
297
|
-
- **Stress Tested**: Handles hundreds of nodes smoothly
|
|
285
|
+
**1. Upgrade the peer dependencies.** `solid-js` and `@solidjs/web` move to matching 2.0 versions.
|
|
298
286
|
|
|
299
|
-
|
|
287
|
+
**2. Store writes: path setters are gone (SolidJS 2.0).** Every `setNodes` / `setEdges` call site using 1.x path syntax becomes a draft callback — the callback's argument is a mutable draft, so mutation is the API. Returning a value instead replaces wholesale.
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
// 0.2.x (SolidJS 1.x) — path syntax
|
|
291
|
+
setNodes(0, "position", "x", (x) => x + 20);
|
|
292
|
+
setEdges((edge) => edge.id === "e1", "animated", true);
|
|
293
|
+
|
|
294
|
+
// 0.3.x (SolidJS 2.0) — draft callback
|
|
295
|
+
setNodes((nodes) => {
|
|
296
|
+
nodes[0]!.position.x += 20;
|
|
297
|
+
});
|
|
298
|
+
setEdges((edges) => {
|
|
299
|
+
const edge = edges.find((e) => e.id === "e1");
|
|
300
|
+
if (edge) edge.animated = true;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// wholesale replacement (re-seeds the flow; rows keyed by id are reused)
|
|
304
|
+
setNodes(() => nextNodes);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**3. `useSolidFlow` reads moved to the reactive `flow` struct.** The flat getters (`getNodes()`, `getEdges()`, `getNode(id)`, `getEdge(id)`, `getInternalNode(id)`, `getViewport()`, `getZoom()`) are removed:
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
// 0.2.x // 0.3.x
|
|
311
|
+
solidFlow.getNodes();
|
|
312
|
+
flow.nodes;
|
|
313
|
+
solidFlow.getViewport();
|
|
314
|
+
flow.viewport;
|
|
315
|
+
solidFlow.getZoom();
|
|
316
|
+
flow.viewport.zoom;
|
|
317
|
+
solidFlow.getNode("a");
|
|
318
|
+
flow.nodes.find((n) => n.id === "a");
|
|
319
|
+
solidFlow.getInternalNode("a");
|
|
320
|
+
useInternalNode(() => "a");
|
|
321
|
+
```
|
|
300
322
|
|
|
301
|
-
|
|
323
|
+
`flow.*` reads are reactive — using them in JSX or a tracked scope subscribes. Commands (`fitView`, `setViewport`, `updateNode`, `deleteElements`, ...) are unchanged and now also available namespaced under `commands`.
|
|
302
324
|
|
|
303
|
-
|
|
304
|
-
- **Custom Nodes** - Creating specialized node types
|
|
305
|
-
- **Edge Types** - Different connection styles
|
|
306
|
-
- **Drag & Drop** - External elements and node creation
|
|
307
|
-
- **Validation** - Connection rules and constraints
|
|
308
|
-
- **Subflows** - Hierarchical node organization
|
|
309
|
-
- **Performance** - Large dataset handling
|
|
310
|
-
- **Accessibility** - Keyboard navigation and screen readers
|
|
325
|
+
**4. New connections are no longer written into your edge store.** In 0.2.x the flow inserted the connected edge into your store before `onConnect` fired. Under 0.3's ownership contract your store owns membership: adopt the connection yourself (see the Quick Start's `onConnect`). Unadopted connections still render, but won't survive a wholesale store replacement.
|
|
311
326
|
|
|
312
|
-
|
|
327
|
+
**5. `onlyRenderVisibleElements` now does what it says.** In 0.2.x the prop was accepted but inert. In 0.3 it opts into unmount culling (off-screen elements are not mounted at all — see [Performance](#performance)), while the CSS culling tier is always on and needs no prop.
|
|
328
|
+
|
|
329
|
+
**6. Smaller signature changes.** `useNodes()` / `useEdges()` return `readonly` arrays; `useHandleEdgeSelect` is removed (it was internal plumbing — select edges through `commands`).
|
|
330
|
+
|
|
331
|
+
## Examples
|
|
332
|
+
|
|
333
|
+
The repository ships a playground with 25+ runnable examples — custom nodes and edges, edge reconnection, drag & drop, subflows, validation, minimap customization, a 10k-node stress test, accessibility, and more:
|
|
313
334
|
|
|
314
335
|
```bash
|
|
336
|
+
bun install
|
|
315
337
|
bun start
|
|
316
338
|
```
|
|
317
339
|
|
|
@@ -341,6 +363,14 @@ bun install
|
|
|
341
363
|
bun start
|
|
342
364
|
```
|
|
343
365
|
|
|
366
|
+
### Testing
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
bun run test # unit + component tests (vitest, jsdom)
|
|
370
|
+
bun run test:ssr # SSR lane (node environment, server builds)
|
|
371
|
+
bun run test:e2e # browser gesture harness (Playwright)
|
|
372
|
+
```
|
|
373
|
+
|
|
344
374
|
### Linting & Formatting
|
|
345
375
|
|
|
346
376
|
```bash
|