Learn / Angular in 10 minutes
Angular in 10 minutes
From npm install to a flow editor with template-defined nodes, connection rules, undo and export — signal-based, zoneless-verified. The same code the Angular demo gallery runs.
1 — Install and mount a canvas
npm install @grafloria/angular @grafloria/element @grafloria/renderer @grafloria/engine
DiagramCanvasComponent is a standalone component with signal-based
two-way bindings. One rule to remember: give the canvas a real height — it fills
its container, and an unstyled custom element collapses to zero.
import { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
@Component({
selector: 'app-flow',
imports: [DiagramCanvasComponent],
template: `
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
[plugins]="true" style="display:block; height:100vh" />
`,
})
export class FlowComponent {
nodes = [
{ id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
{ id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
];
edges = [{ id: 'e1', source: 'a', target: 'b' }];
}
That's a working editor: drag, connect, pan, zoom — plus minimap, zoom controls and a
dotted background from [plugins]="true" (lazy-loaded; omit the binding and
none of it is downloaded). Plain array fields work; signal([...]) works
identically — the bindings are model()-based either way. No stylesheet
import, no NgModule, no provider is required.
2 — Custom nodes are ng-templates
Declare a template for a node type and every node of that type renders
through it — declaring the template is the opt-in, no extra flag needed:
import { DiagramCanvasComponent, GrafloriaNodeDefDirective } from '@grafloria/angular';
@Component({
selector: 'app-flow',
imports: [DiagramCanvasComponent, GrafloriaNodeDefDirective],
template: `
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
style="display:block; height:100vh">
<ng-template grafloriaNode="card" let-data="data">
<div class="card">
<div class="title">{{ data['title'] }}</div>
<div class="owner">owner: {{ data['owner'] }}</div>
</div>
</ng-template>
</grafloria-diagram-canvas>
`,
styles: [`.card { height: 100%; background: #fff; border: 1.5px solid #94A5F0;
border-radius: 12px; padding: 10px 14px; box-sizing: border-box; }
.title { font-weight: 700; } .owner { font-size: 12px; color: #5A6478; }`],
})
export class FlowComponent {
nodes = [
{ id: 'a', type: 'card', position: { x: 80, y: 90 }, size: { width: 230, height: 110 },
data: { title: 'Build', owner: 'CI' } },
{ id: 'b', type: 'card', position: { x: 430, y: 90 }, size: { width: 230, height: 110 },
data: { title: 'Deploy', owner: 'CD' } },
];
edges = [{ id: 'e1', source: 'a', target: 'b' }];
}
The template context gives you let-data="data" (the node's payload),
let-node (the live NodeModel) and
let-engine="engine". An <ng-template grafloriaNode> with
no value is the wildcard fallback for every type without its own template.
Live: ng-template custom nodes →
3 — Ports and connection rules
Ports are part of the node spec — a side, a direction, optionally a
dataType:
nodes = [
{ id: 'src', position: { x: 120, y: 260 }, size: { width: 130, height: 70 }, label: 'number src',
ports: [{ id: 'out', side: 'right' as const, type: 'output', dataType: 'number' }] },
{ id: 'num', position: { x: 640, y: 140 }, size: { width: 130, height: 70 }, label: 'number in',
ports: [{ id: 'nin', side: 'left' as const, type: 'input', dataType: 'number' }] },
];
Type compatibility is registered once, app-wide; custom rules are a validator that
returns true to allow or a string to veto with a reason. Both come from the
framework-agnostic packages:
import { registerConnectionValidator, clearConnectionValidators } from '@grafloria/renderer';
import { portTypeRegistry } from '@grafloria/element';
portTypeRegistry.registerAll([
{ name: 'number', color: '#2563eb', compatibleWith: ['number'] },
{ name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);
export class FlowComponent implements AfterViewInit, OnDestroy {
private dispose?: () => void;
ngAfterViewInit() {
this.dispose = registerConnectionValidator(({ sourcePort, targetPort }) => {
if (sourcePort?.type === 'output' && targetPort?.type === 'output')
return 'an output cannot feed another output';
return true;
});
}
ngOnDestroy() { this.dispose?.(); clearConnectionValidators(); }
}
ngOnDestroy or they leak
across routes. Ports show on hover by default; to show them always, reach the engine
after view init:
this.canvas().activeEngine()?.setInteractionConfig({ portVisibility: 'always' }).4 — Undo, viewport, engine access
⌘Z/Ctrl+Z works with zero wiring. For your own toolbar, the
canvas surfaces the essentials directly; everything else lives on the engine, reached
with viewChild after the view initializes:
import { Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
export class FlowComponent {
canvas = viewChild.required(DiagramCanvasComponent);
undo() { void this.canvas().undo(); }
redo() { void this.canvas().redo(); }
fit() { this.canvas().fitToContent(40); }
// engine escape hatch — canUndo, copy/paste, selection, interaction config …
get engine() { return this.canvas().activeEngine(); }
}
activeEngine() is available from ngAfterViewInit
onward — engine-touching setup belongs there, not in the constructor.5 — Auto-layout
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
[layout]="'elk'" style="display:block; height:100vh" />
<!-- configured: -->
[layout]="{ name: 'dagre', options: { direction: 'TB', rankSpacing: 80 } }"
Registered names: auto, elk, dagre,
layered, tree, grid, circular,
radial, force, spectral, community.
ELK loads lazily in a Worker (a separate ~432 KB gz chunk, only when first used).
The binding re-runs on value change only — it never fights a drag; call
applyLayout() on the component to re-run on demand, and listen with
(layoutDone).
6 — Save, load, export
// snapshot / restore (JSON)
saved: SerializedDiagram | null = null; // type from '@grafloria/engine'
save() { this.saved = this.canvas().snapshot(); }
restore() { if (this.saved) this.canvas().loadSnapshot(this.saved); }
// Mermaid-compatible text round-trip
text = '';
export() { this.text = this.canvas().exportText(); }
load() { this.canvas().loadText(this.text); }
// images — PNG returns a data: URL, SVG returns the raw string
async downloadPng() {
const url = await this.canvas().exportDiagram('png', { scale: 2 });
const a = document.createElement('a');
a.href = url; a.download = 'diagram.png'; a.click();
}
exportDiagram('pdf')) produces a real vector PDF:
paths stay paths, text stays selectable.Where next
- Every demo as an Angular component — 100+ routes, source shown.
- The model — what nodes, ports, links and groups really are.
- Kits —
<grafloria-dashboard>withng-template grafloriaWidgetwidgets. - App-wide defaults:
provideGrafloria({ theme })in your bootstrap providers; a canvas-level[theme]binding always wins. - Zoneless: the binding is verified against zoneless change detection — no
zone.jsassumptions anywhere.