Register a tool against a mounted diagram, let it own a pointer gesture, and dispose the registration when the host is removed.
When to use this
Use a canvas tool when the gesture belongs to your application rather than to a diagram node or link. Grafloria ships a rectangle tool, so this example uses it instead of implementing rectangle geometry. A drag on empty canvas creates a real NodeModel; the node is rendered at the dragged position and size.
registerTool() returns a disposer. Keep that function with the mounted component and call it during teardown. Registering another tool with the same id replaces the previous registration; disposing the new registration restores the previous tool.
Mount the tool
The tool needs the live diagram model, viewport, container, and a repaint function. Build that small host from the mounted DiagramInstance. The host adapter is structural: it does not create a second diagram.
JavaScript
html<div id="diagram" style="height:100vh"></div>
jsconst diagramElement = document.querySelector('#diagram');
if (!(diagramElement instanceof HTMLElement)) {
throw new Error('Expected #diagram');
}
diagramElement.style.height = '100vh';
import { render } from '@grafloria/element';
import {
registerTool,
createRectangleTool,
} from '@grafloria/renderer';
const host = diagramElement;
const instance = render({
nodes: [{
id: 'box1',
position: { x: 120, y: 100 },
size: { width: 300, height: 180 },
label: 'Box',
style: { shape: 'rectangle', fill: '#dbeafe', stroke: '#2563eb', strokeWidth: 2 },
}],
edges: [],
}, host);
const toolHost = {
getModel: () => instance.getModel(),
viewport: instance.viewport,
container: host,
render: () => instance.renderNow(),
getEngine: () => instance.getEngine(),
};
const disposeTool = registerTool(createRectangleTool(toolHost, {
fill: '#dbeafe',
stroke: '#2563eb',
strokeWidth: 2,
label: 'Box',
}));
// Call this when the page or the diagram host is removed.
export function disposeDiagram() {
disposeTool();
instance.dispose();
}
Give #diagram a resolved height, for example <div id="diagram" style="height: 100vh"></div>. Press on empty canvas and drag corner to corner. The tool claims the gesture, then adds one rectangular node and repaints the mounted instance.
Try the working rectangle demo and its source.
Angular
Register after the canvas exposes its engine, and dispose in ngOnDestroy.
tsimport { AfterViewInit, Component, ElementRef, OnDestroy, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { registerTool, createRectangleTool } from '@grafloria/element';
import type { WhiteboardHost } from '@grafloria/element';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<div #host style="display:block;height:100vh">
<grafloria-diagram-canvas
[(nodes)]="nodes"
[(edges)]="edges"
style="display:block;height:100%" />
</div>
`,
})
export class RectangleComponent implements AfterViewInit, OnDestroy {
readonly canvas = viewChild.required(DiagramCanvasComponent);
readonly host = viewChild.required<ElementRef<HTMLElement>>('host');
nodes = [{
id: 'box1', position: { x: 120, y: 100 }, size: { width: 300, height: 180 }, label: 'Box',
}];
edges = [];
private disposeTool: (() => void) | undefined;
ngAfterViewInit(): void {
const canvas = this.canvas();
const engine = canvas.activeEngine();
const model = engine?.getDiagram();
const viewport = canvas.viewportController();
if (!model || !viewport) return;
const instanceHost: WhiteboardHost = {
getModel: () => model,
viewport,
container: this.host().nativeElement,
render: () => canvas.scheduleRender(),
getEngine: () => engine ?? null,
};
this.disposeTool = registerTool(createRectangleTool(instanceHost, {
fill: '#dbeafe',
stroke: '#2563eb',
strokeWidth: 2,
label: 'Box',
}));
}
ngOnDestroy(): void {
this.disposeTool?.();
}
}
Angular owns the mounted element, while the tool uses the instance-backed host only for the gesture.
Qwik
In Qwik, register the tool from the browser-side onInit$ callback and keep its disposer in a non-serializable signal. Call that disposer from useVisibleTask$ cleanup when the component leaves the page.
textconst dispose = noSerialize(registerTool(createRectangleTool({ getModel: () => instance.getModel(), viewport: instance.viewport, container: host, render: () => instance.renderNow(), getEngine: () => instance.getEngine(), }, { fill: '#dbeafe', stroke: '#2563eb', strokeWidth: 2, label: 'Box' }))); useVisibleTask$(({ cleanup }) => { cleanup(() => dispose?.()); });
React
Store the disposer in a ref so a re-render does not lose it, and dispose it in the unmount effect.
tsximport { useEffect, useRef } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import { registerTool, createRectangleTool } from '@grafloria/element';
import type { WhiteboardHost } from '@grafloria/element';
export default function RectangleDemo() {
const hostRef = useRef<HTMLDivElement | null>(null);
const disposeRef = useRef<(() => void) | undefined>(undefined);
useEffect(() => () => disposeRef.current?.(), []);
const onInit = (instance: DiagramInstance) => {
const host = hostRef.current;
if (!host) return;
const instanceHost: WhiteboardHost = {
getModel: () => instance.getModel(),
viewport: instance.viewport,
container: host,
render: () => instance.renderNow(),
getEngine: () => instance.getEngine(),
};
disposeRef.current = registerTool(createRectangleTool(instanceHost, {
fill: '#dbeafe',
stroke: '#2563eb',
strokeWidth: 2,
label: 'Box',
}));
};
return (
<div ref={hostRef} style={{ display: 'block', height: '100vh' }}>
<GrafloriaFlow defaultNodes={[{ id: 'box1', position: { x: 120, y: 100 }, size: { width: 300, height: 180 }, label: 'Box', style: { shape: 'rectangle', fill: '#dbeafe', stroke: '#2563eb', strokeWidth: 2 } }]} defaultEdges={[]} onInit={onInit} style={{ display: 'block', height: '100%' }} />
</div>
);
}
Vue
Capture the instance from @init, then dispose the registration from onBeforeUnmount.
vue<script setup lang="ts"> import { onBeforeUnmount, ref } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance } from '@grafloria/vue'; import { registerTool, createRectangleTool } from '@grafloria/element'; import type { WhiteboardHost } from '@grafloria/element'; const host = ref<HTMLElement | null>(null); let disposeTool: (() => void) | undefined; function onInit(instance: DiagramInstance): void { if (!host.value) return; const instanceHost: WhiteboardHost = { getModel: () => instance.getModel(), viewport: instance.viewport, container: host.value, render: () => instance.renderNow(), getEngine: () => instance.getEngine(), }; disposeTool = registerTool(createRectangleTool(instanceHost, { fill: '#dbeafe', stroke: '#2563eb', strokeWidth: 2, label: 'Box', })); } onBeforeUnmount(() => disposeTool?.()); </script> <template> <div ref="host" style="height:100vh"> <GrafloriaFlow :default-nodes="[{ id: 'box1', position: { x: 120, y: 100 }, size: { width: 300, height: 180 }, label: 'Box', style: { shape: 'rectangle', fill: '#dbeafe', stroke: '#2563eb', strokeWidth: 2 } }]" style="height:100%" @init="onInit" /> </div> </template>
Tool options
These are the options used by the shipped rectangle implementation in the example.
| Option | Type | Default | What it does |
|---|---|---|---|
fill | string | Not specified | Sets the new rectangle's fill. |
stroke | string | Not specified | Sets the new rectangle's outline. |
strokeWidth | number | Not specified | Sets the outline width. |
label | string | Not specified | Sets the label on each new rectangle. |
Arbitration and cleanup
hitTest decides which registered tool claims a pointerdown. When several tools claim the same gesture, the highest explicit priority wins; ties use registration order, which is not a contract to rely on. A point-specific tool should use a higher priority than a broad mode tool when both can claim the same pointerdown.
Dispose every registration owned by a routed page or component. The disposer also calls the tool's cleanup when the registration is removed, and restores a previous tool with the same id. Do not leave a global registration active after its diagram disappears.
Related
- How Grafloria works explains the instance, model, and engine layers.
- Events and interaction covers the interaction model beneath canvas gestures.
- Customize nodes covers rendering node content rather than claiming a canvas gesture.
Was this page helpful?