Use QRL callbacks and instance signals to connect a resumable diagram to sibling controls. You get two connected nodes, a Fit view button, selection and camera readouts, and a server-rendered version that becomes interactive in the browser.
The binding is a thin skin over the shared model: keep plain specs in resumable state and use the live instance for rendering operations. Start with an existing Qwik 1.x project using @builder.io/qwik ^1.5.0 and its optimizer; see the Qwik quick start for project setup.
bashnpm install @grafloria/qwik @grafloria/engine @grafloria/renderer @grafloria/element @builder.io/qwik
1. Keep the diagram input as data
Share the GrafloriaFlow input data—typed with NodeSpec and EdgeSpec—between the sibling-subscription and server-adoption examples below; see the React quick start for basic node-and-edge setup.
tsimport type { NodeSpec, EdgeSpec } from '@grafloria/qwik';
export const nodes: NodeSpec[] = [
{ id: 'plan', label: 'Plan', position: { x: 60, y: 80 },
size: { width: 160, height: 64 } },
{ id: 'ship', label: 'Ship', position: { x: 320, y: 80 },
size: { width: 160, height: 64 } },
];
export const edges: EdgeSpec[] = [
{ id: 'plan-ship', source: 'plan', target: 'ship',
sourceHandle: 'right', targetHandle: 'left' },
];
2. Connect QRL callbacks and sibling subscriptions
Wrap the canvas and its toolbar in GrafloriaProvider. useGrafloria returns the shared instance signal; it is undefined until the flow mounts. useSelection returns the current selected nodes and edges, and useViewport returns the camera's zoom and world origin.
Use the DiagramInstance signal to show canvas readiness beside the sibling toolbar; see the Qwik quick start for onInit$, noSerialize() and QRL callback setup.
For a sibling side effect, useOnSelectionChange$ fires a QRL on each selection change and removes its subscription automatically. Here it counts selection notifications rather than storing live models in application state.
useOnSelectionChangeQrl accepts an explicitly $()-wrapped handler. The second subscription logs selected node ids to the browser console.
tsximport { $, component$, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import {
GrafloriaFlow, GrafloriaProvider, useGrafloria,
useSelection, useViewport, useOnSelectionChange$, useOnSelectionChangeQrl,
type DiagramInstance,
} from '@grafloria/qwik';
import { nodes, edges } from './diagram-data';
const Toolbar = component$(() => {
const instance = useGrafloria();
const selection = useSelection();
const viewport = useViewport();
const notifications = useSignal(0);
useOnSelectionChange$(() => {
notifications.value += 1;
});
useOnSelectionChangeQrl($(({ nodes: selectedNodes }) => {
console.log('Selected nodes:', selectedNodes.map((node) => node.id));
}));
return (
<div>
<button type="button" disabled={!instance.value}
onClick$={() => instance.value?.fitView(40)}>
Fit view
</button>
<p>
Selected: {selection.value.nodes.length} node(s),{' '}
{selection.value.edges.length} edge(s).{' '}
Zoom: {viewport.value.zoom.toFixed(2)}.{' '}
Origin: {viewport.value.x.toFixed(0)}, {viewport.value.y.toFixed(0)}.
</p>
<p>Selection notifications: {notifications.value}</p>
</div>
);
});
export default component$(() => {
const instance = useSignal<NoSerialize<DiagramInstance>>();
const clicked = useSignal('None');
return (
<GrafloriaProvider>
<Toolbar />
<p>Canvas: {instance.value ? 'Ready' : 'Starting'}. Last clicked node: {clicked.value}</p>
<GrafloriaFlow
defaultNodes={nodes}
defaultEdges={edges}
fitView
style={{ height: '400px' }}
onInit$={(diagram) => { instance.value = noSerialize(diagram); }}
onNodeClick$={({ node }) => { clicked.value = node.id; }}
/>
</GrafloriaProvider>
);
});
The mounted canvas shows Plan connected to Ship. Click a node to update the clicked-node text and selection readout. Pan or zoom to update the camera readout; Fit view frames the content. defaultNodes and defaultEdges seed the instance once, so the instance owns subsequent edits.
For controlled state instead, pass nodes and edges from typed signals and write the returned specs back through onNodesChange$ and onEdgesChange$. Those callbacks receive spec arrays, not the live models supplied by selection callbacks. See State and event flow for the ownership decision.
3. Adopt the server SVG with its CSS
Pass the StaticRenderResult from renderToStaticSVG to the Qwik flow's ssr prop and emit its CSS separately to turn the server preview into an interactive canvas; see React: state and subscriptions for the static result's fields.
In an SSR-rendered component, build the result in useTask$, following the resumable demo. Keep that result as plain data and pass it to ssr together with the same node and edge specs. This example emits the stylesheet beside the flow; when composing your document shell, place result.css in a <style> in the document head so the first server response already carries the diagram's styles.
tsximport { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { GrafloriaFlow, renderToStaticSVG, type StaticRenderResult } from '@grafloria/qwik';
import { nodes, edges } from './diagram-data';
export default component$(() => {
const result = useSignal<StaticRenderResult>();
const ready = useSignal(false);
useTask$(() => {
result.value = renderToStaticSVG({
nodes, edges, width: 640, height: 400,
instanceId: 'plan-ship-ssr', fitView: true,
});
});
return (
<>
<style dangerouslySetInnerHTML={result.value?.css ?? ''} />
<p>{ready.value ? 'Interactive' : 'Server preview'}</p>
<GrafloriaFlow
defaultNodes={nodes}
defaultEdges={edges}
ssr={result.value}
style={{ height: '400px' }}
onInit$={() => { ready.value = true; }}
/>
</>
);
});
View the server response source: Plan, Ship and their SVG are already in the HTML. At document ready, the flow builds a live instance using the snapshot and adopts the existing DOM instead of rebuilding it. The status changes to Interactive, and you can drag the nodes. Resumption avoids re-walking the component tree; it does not defer the diagram's initialization until the first click.
The snapshot carries instance scope, canvas width and height, zoom, and viewport origin—not a saved document. The client rebuilds the model from your specs. Use Save and restore documents for persistence. Custom HTML nodes mount in the browser; they are not part of the server-rendered SVG.
Options that matter
For the flow options below, see GrafloriaFlowProps. Static rendering takes StaticRenderOptions.
| Option | Type | Default | What it does |
|---|---|---|---|
defaultNodes, defaultEdges | NodeSpec[], EdgeSpec[] | Empty arrays when no controlled inputs exist | Seed the instance on mount. |
nodes, edges | NodeSpec[], EdgeSpec[] | Unset | Reconcile controlled specs into the instance. Wire their change callbacks back to your state. |
ssr | { html: string; snapshot: HydrationSnapshot } | Unset | Emit server markup and adopt it in the browser. Supply its CSS separately. |
Static width, height | number | 800, 600 | Set the server canvas dimensions in CSS pixels. |
Static instanceId | string | 'grafloria-ssr' | Scope the diagram. Give multiple server-rendered diagrams distinct ids. |
Static fitView | boolean | false | Frame content instead of using the supplied camera. |
Static fitPadding | number | 40 | Set fit padding in CSS pixels. |
HydrationSnapshot is returned by static rendering; pass it through rather than reconstructing it.
Keep browser objects out of resumable state
The provider's instance signal is already marked noSerialize(). Your own instance signal needs the explicit marker shown above. Its value does not survive serialization: the browser builds a fresh instance, and onInit$ supplies that instance again. Do not close over a live instance directly in another QRL; capture its signal and read .value when the handler runs.
Create live transports and shared stores in useVisibleTask$, retain them with noSerialize(), and tear them down when their owner unmounts. The flow's collaboration options are fixed for the instance's lifetime, so create the options before mounting a flow that consumes them. See Documents and kits for live-object handling and Qwik: custom content and kits for browser-built spec$ factories.
Keep the provider above both the canvas and its subscribing siblings. Outside a provider, the hooks have no instance to reach; useGrafloria() stays undefined and logs a development warning. Use a separate provider for each independently controlled canvas.
Load the event loader in client-only apps
An SSR page carries Qwik's event loader automatically. If your app only calls Qwik's render() in the browser, add the loader once before rendering; without it, DOM handlers such as Fit view do not fire.
Use this entry only for a client-only app. It mounts the editor from step 2 into a newly created element in the browser.
tsximport { render } from '@builder.io/qwik';
import { QWIK_LOADER } from '@builder.io/qwik/loader';
import Editor from './editor';
const loader = document.createElement('script');
loader.textContent = QWIK_LOADER;
document.head.appendChild(loader);
const container = document.createElement('div');
document.body.appendChild(container);
void render(container, <Editor />);
Live demos and related tasks
- Qwik gallery — run the framework's diagrams and inspect their source.
- Server-side export — inspect a static SVG preview; unlike the adopted flow above, that preview is not interactive.
- Resumable sample source — the server markup and snapshot handoff.
- Instance and lifecycle — instance operations and teardown.
- Qwik: custom content and kits — custom containers and QRL spec factories.
Was this page helpful?