Learn / Dashboards in Angular
Dashboards in Angular
A dashboard is data: [views] declares the whole board and the kit wires the 12-column pack grid, drag/resize gestures and undo. <grafloria-dashboard> adds the Angular idioms on top — a two-way active view, a typed handle, and ng-template widgets.
A board in one binding
import { Component } from '@angular/core';
import { GrafloriaDashboardComponent } from '@grafloria/angular';
@Component({
selector: 'app-board',
imports: [GrafloriaDashboardComponent],
template: `
<grafloria-dashboard [views]="views" [(activeView)]="tab"
style="display:block; height:100vh" />
`,
})
export class BoardComponent {
tab: string | undefined = 'overview';
views = [
{ id: 'overview', name: 'Overview', widgets: [
{ id: 'kpi-revenue', kind: 'kpi', span: 3, rows: 1,
data: { label: 'Total revenue', value: '$6.81M', delta: 12.4, deltaLabel: 'vs last qtr',
spark: [42, 45, 47, 51, 50, 55, 59, 61, 60, 65, 71, 76] } },
{ id: 'trend', kind: 'line', span: 6, rows: 2, title: 'Revenue trend',
data: { series: [{ name: 'Revenue', values: [42, 45, 47, 51, 50, 55, 59, 61, 60, 65, 71, 76] }],
labels: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'] } },
{ id: 'mix', kind: 'donut', span: 6, rows: 2, title: 'Revenue by region',
data: { slices: [
{ label: 'EMEA', value: 2.9, color: '#3B52D9' },
{ label: 'AMER', value: 2.4, color: '#94A5F0' },
{ label: 'APAC', value: 1.5, color: '#059669' },
], centerLabel: '$6.8M' } },
{ id: 'bars', kind: 'bar', span: 6, rows: 2, title: 'Deals by quarter',
data: { bars: [
{ label: 'Q1', value: 210 }, { label: 'Q2', value: 260 },
{ label: 'Q3', value: 245 }, { label: 'Q4', value: 292 },
] } },
]},
];
}
That is a complete interactive dashboard: widgets drag and resize on a gravity-packed
grid, every committed gesture is one undo step, and each kind above is
painted by a built-in renderer. Widgets flow in declaration order, wrapping at the
column count — the common case needs no coordinates at all (add x/y
to pin a cell explicitly, pinned: true for a tile no reflow may move).
As with the canvas: give the element a real height.
The component surface
[views]—DashboardViewSpec[]: a multi-view (tabbed) board. Each view is{ id, name?, widgets, columns?, width?, height? }— the last three override the board geometry per view.[widgets]—DashboardWidgetSpec[]: single-view shorthand. Mutually exclusive withviews.[options]—Partial<DashboardOptions>: columns (default 12), gap (default 8),sizing: 'fit' | 'grow',rowHeight,float,rtl, andresponsive(derive the live column count from board width — bycolumnWidthor namedbreakpoints).[(activeView)]— two-way active view id; the tab pattern.(ready)— emits the typedDashboardHandleonce the board is live.(layoutChange)—{ viewId, widgets }, mirroring the kit's committed gestures: drag, resize, add, remove.- Methods:
getHandle()(undefined before first paint) andsnapshot().
A widget spec is small: { id, kind?, span?, rows?, x?, y?, pinned?, data?,
title? } — span defaults to 3 columns, rows to 1, and
data is your payload, handed back untouched to whatever renders the
widget.
Built-in widget kinds
Six kinds render with zero code of yours — their data shapes, from the
kit's own type declarations:
kind: 'kpi' → { label?, value?, delta?, deltaLabel?, spark?: number[] }
// headline number; delta paints up/green or down/red; spark = trend line
kind: 'line' → { series?: number[] | { name?, values }[], labels?: string[] }
// a bare number[] is the single-series shorthand
kind: 'bar' → { bars?: { label?, value? }[] }
kind: 'donut' → { slices?: { label?, value?, color? }[], centerLabel?, centerCaption? }
kind: 'funnel' → { stages?: { label?, value? }[] } // each stage scaled vs the first
kind: 'table' → { columns?: string[], rows?: (string | number)[][] }
// numbers right-align on their own
Any other kind string gets a titled placeholder frame — useful while a
board's data contracts are still settling.
Custom widgets: ng-template grafloriaWidget
The node-template idiom, applied to boards. A widget whose kind matches
a template renders through it — full Angular change detection, components, pipes and
event handlers; kinds without a template fall back to the built-in painters above.
grafloriaWidget with no value is the wildcard for any kind without an
exact template.
import { GrafloriaDashboardComponent, GrafloriaWidgetDefDirective } from '@grafloria/angular';
@Component({
imports: [GrafloriaDashboardComponent, GrafloriaWidgetDefDirective, OrdersCardComponent],
template: `
<grafloria-dashboard [views]="views" style="display:block; height:100vh">
<ng-template grafloriaWidget="orders" let-widget let-data="data">
<app-orders-card [title]="widget.title" [orders]="data['orders']" />
</ng-template>
</grafloria-dashboard>
`,
})
The context mirrors the node one: $implicit (let-widget)
is the full DashboardWidgetSpec, let-data="data" is its
payload as Record<string, unknown> — index syntax again. Your
template is stamped into a wrapper the component sizes to 100% × 100% of
the widget's cell, so a root with height: 100% fills the tile.
Tabs — the active-view pattern
Multiple views park all but one off-camera; showView frames the chosen
one. The component folds that into [(activeView)], so tab buttons are just
writes to your own property:
<nav>
@for (v of views; track v.id) {
<button [class.on]="tab === v.id" (click)="tab = v.id">{{ v.name }}</button>
}
</nav>
<grafloria-dashboard [views]="views" [(activeView)]="tab"
style="display:block; height:calc(100vh - 48px)" />
On mount, a pre-set activeView is applied; if you left it undefined the
component reflects the board's boot view back into the binding (deferred a microtask —
writing it in the same change-detection pass would be NG0100).
Live: grids, float, RTL, responsive columns and pinning — one tab each →
Persistence: layoutChange + the snapshot round-trip
<grafloria-dashboard [views]="views" (ready)="handle = $event" (layoutChange)="persistView($event.viewId, $event.widgets)" />
(layoutChange) fires after any committed gesture with the view whose
layout changed — the natural autosave hook. For whole-board persistence,
snapshot() returns the handle's toJSON(): every
DashboardOptions field except the function seams
(renderWidget, onLayoutChange), read from the live
board — a column count or sizing mode the user changed after mount is what you get
back. Feed it back to [views]/[options] and the identical
board rebuilds; that is the kit's round-trip contract.
Beyond that, the DashboardHandle from (ready) is the
live-control surface: showView, addWidget (creates the node
and commits it as one undoable step), setColumns / setRtl /
setSizing / setFloat live switches, metrics(),
refresh() after out-of-band mutations, and
exportIds(viewId) — the node ids one view occupies, for scoping an export
to a single board instead of the whole multi-view model.
Dashboard or canvas?
Both render through the same engine — which is why dashboard gestures are undoable and boards serialize like any diagram. Choose by what the user manipulates:
<grafloria-dashboard>when the content is widgets on a grid: cells, spans, gravity packing, pinning, RTL, responsive columns. You give up free placement and get layout discipline for free.<grafloria-diagram-canvas>when the content is a graph: nodes anywhere, edges, ports, routing, layout algorithms. See Custom nodes in Angular.
The kit family — dashboards, ER, UML — shares this data-first shape; the framework-agnostic story is in Kits.
Where next
- Kits —
dashboard(),erDiagram(),umlDiagram()from plain data, in any framework. - State, signals & tooling — the canvas-side binding surface this page builds on.
- The model — what those widget nodes really are underneath.