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.

flow.component.ts
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:

flow.component.ts
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(); }
}
Validators are global — dispose in 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' }).

Live: typed ports →

Live: connection validation →

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.

Live: undo/redo buttons →

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).

Live: ELK layout →

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();
}
Export needs a painted canvas — calling it before the first paint throws a clear error. PDF export (exportDiagram('pdf')) produces a real vector PDF: paths stay paths, text stays selectable.

Live: save & restore →

Live: PNG/SVG download →

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> with ng-template grafloriaWidget widgets.
  • 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.js assumptions anywhere.