Build a small palette beside a mounted diagram; choosing a shape adds a labelled node to the live model in the next available grid position.
Use this pattern when your app owns a short list of node types and wants readers to add them without editing diagram data directly. The framework bindings mount a canvas; its instance reaches the engine, which adds each selected node to the diagram.
Build a palette
Start with an empty diagram. In JavaScript, React and Vue, keep the live DiagramInstance from the component's init callback and call getEngine().addNode() from each palette button. Angular gets its active engine from the canvas component when a button is clicked; Qwik updates controlled node data. The engine returns a promise for the node it adds; each sample places new nodes in a grid so their labels stay visible beside previous choices.
Unlike a workflow that starts from a fixed graph, each palette choice adds one labelled rect at the next grid position; see Build runnable workflows for the framework-specific mounting patterns.
jsimport { render } from '@grafloria/element';
const root = document.createElement('div');
root.style.cssText = 'display:grid;grid-template-columns:160px 1fr;height:420px';
document.body.append(root);
const palette = document.createElement('div');
palette.style.cssText = 'display:flex;flex-direction:column;gap:8px;padding:12px';
const canvas = document.createElement('div');
canvas.style.height = '420px';
root.append(palette, canvas);
const instance = render({ nodes: [], edges: [] }, canvas);
const kinds = ['Source', 'Filter', 'Sink'];
async function addNode(kind) {
const count = instance.getModel().getNodes().length;
await instance.getEngine().addNode({
type: 'rect',
position: { x: 40 + (count % 4) * 170, y: 40 + Math.floor(count / 4) * 90 },
size: { width: 140, height: 64 },
data: { label: kind },
});
instance.renderNow();
}
for (const kind of kinds) {
const button = document.createElement('button');
button.textContent = kind;
button.addEventListener('click', () => { void addNode(kind); });
palette.append(button);
}
tsimport { Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
@Component({
selector: 'app-node-palette',
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<div style="display:grid;grid-template-columns:160px 1fr;height:420px">
<div style="display:flex;flex-direction:column;gap:8px;padding:12px">
<button type="button" (click)="addNode('Source')">Source</button>
<button type="button" (click)="addNode('Filter')">Filter</button>
<button type="button" (click)="addNode('Sink')">Sink</button>
</div>
<grafloria-diagram-canvas [nodes]="nodes" [edges]="edges" style="display:block;height:100%" />
</div>
`,
})
export class NodePaletteComponent {
readonly canvas = viewChild.required(DiagramCanvasComponent);
readonly nodes: NodeSpec[] = [];
readonly edges: EdgeSpec[] = [];
async addNode(kind: string): Promise<void> {
const engine = this.canvas().activeEngine();
if (!engine) return;
const diagram = engine.getDiagram() ?? engine.createDiagram();
const count = diagram.getNodes().length;
await engine.addNode({
type: 'rect',
position: { x: 40 + (count % 4) * 170, y: 40 + Math.floor(count / 4) * 90 },
size: { width: 140, height: 64 },
data: { label: kind },
});
}
}
tsximport { $, component$, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { NodeSpec } from '@grafloria/renderer';
export default component$(() => {
const nodes = useSignal<NodeSpec[]>([]);
const kinds = ['Source', 'Filter', 'Sink'];
return (
<div style={{ display: 'grid', gridTemplateColumns: '160px 1fr', height: '420px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '12px' }}>
{kinds.map((kind) => (
<button key={kind} type="button" data-kind={kind} onClick$={$((_event: Event, button: HTMLButtonElement) => {
const selectedKind = button.dataset['kind'];
if (!selectedKind) return;
const count = nodes.value.length;
nodes.value = [...nodes.value, {
id: `palette-${count + 1}`,
type: 'rect',
position: { x: 40 + (count % 4) * 170, y: 40 + Math.floor(count / 4) * 90 },
size: { width: 140, height: 64 },
data: { label: selectedKind },
}];
})}>
{kind}
</button>
))}
</div>
<div style={{ height: '420px' }}>
<GrafloriaFlow nodes={nodes.value} defaultEdges={[]} />
</div>
</div>
);
});
tsximport { useRef } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
export function NodePalette() {
const api = useRef<DiagramInstance | null>(null);
const kinds = ['Source', 'Filter', 'Sink'];
async function addNode(kind: string): Promise<void> {
const instance = api.current;
if (!instance) return;
const count = instance.getModel().getNodes().length;
await instance.getEngine().addNode({
type: 'rect',
position: { x: 40 + (count % 4) * 170, y: 40 + Math.floor(count / 4) * 90 },
size: { width: 140, height: 64 },
data: { label: kind },
});
instance.renderNow();
}
return (
<div style={{ display: 'grid', gridTemplateColumns: '160px 1fr', height: 420 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: 12 }}>
{kinds.map((kind) => <button key={kind} type="button" onClick={() => { void addNode(kind); }}>{kind}</button>)}
</div>
<div style={{ height: 420 }}>
<GrafloriaFlow defaultNodes={[]} defaultEdges={[]} onInit={(instance) => { api.current = instance; }} />
</div>
</div>
);
}
vue<script setup lang="ts"> import { ref } from 'vue'; import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue'; const api = ref<DiagramInstance | null>(null); const kinds = ['Source', 'Filter', 'Sink']; async function addNode(kind: string): Promise<void> { const instance = api.value; if (!instance) return; const count = instance.getModel().getNodes().length; await instance.getEngine().addNode({ type: 'rect', position: { x: 40 + (count % 4) * 170, y: 40 + Math.floor(count / 4) * 90 }, size: { width: 140, height: 64 }, data: { label: kind }, }); instance.renderNow(); } </script> <template> <div style="display:grid;grid-template-columns:160px 1fr;height:420px"> <div style="display:flex;flex-direction:column;gap:8px;padding:12px"> <button v-for="kind in kinds" :key="kind" type="button" @click="addNode(kind)">{{ kind }}</button> </div> <div style="height:420px"> <GrafloriaFlow :default-nodes="[]" :default-edges="[]" @init="api = $event" /> </div> </div> </template>
Click a palette button to add a labelled rect node. JavaScript, React and Vue call addNode() on the mounted instance; Angular calls addNode() on its active engine, and Qwik updates its controlled node array. Each addition occupies the next grid position.
Options that matter
| Option | Type | Default | What it does |
|---|---|---|---|
defaultNodes | NodeSpec[] | Not specified | Seeds the diagram once; the JavaScript, React and Vue samples pass an empty array. Angular and Qwik pass empty controlled node arrays. |
defaultEdges | EdgeSpec[] | Not specified | Seeds the diagram's edges; the samples pass an empty array. |
onInit / onInit$ | (instance: DiagramInstance) => void | Not specified | Gives the mounted instance to the JavaScript, React and Vue palette code. Angular reads the active engine from its canvas component; Qwik uses controlled node data. |
addNode config type | string | Required | Selects the node type; the sample uses rect. |
addNode config position | Point | Required | Sets the node's top-left position in diagram coordinates. |
addNode config size | Size | Optional | Sets the node's width and height. |
addNode config data | any | Optional | Carries node data; data.label supplies the visible label in these samples. |
Pitfalls
- If you later replace externally edited node data with
setNodes()orloadText(), those calls reconcile rather than rebuild: ids that remain keep their live objects. Clear the current edges and nodes first, then apply the edited data. See Edit and copy nodes for node-data workflows. - This palette uses the built-in
rectnode and a data label. For custom-rendered nodes, follow Create custom nodes.
See it running
The Drag & drop demo shows a drag-and-drop palette whose nodes land at the drop point. For categorized, searchable built-in shapes, see the Stencil palette demo.
Was this page helpful?