@ogidor/dashboard
v1.1.7
Published
A content-agnostic drag-and-drop dashboard library for Angular 15. Multi-workspace, persistent layout, pop-out windows, live cross-tab sync.
Maintainers
Readme
@ogidor/dashboard
A lightweight, content-agnostic drag-and-drop dashboard library for Angular 15. Drop it into any project, supply your own card content, and get a fully featured multi-workspace grid with pluggable persistence, pop-out windows, and live cross-tab sync — out of the box.
Table of Contents
- Features
- Installation
- Quick Start
- API Reference
- Models
- Theming
- Labels & i18n
- Pop-out Windows
- Persisting Layouts
- SSR / Angular Universal
- Full Integration Example
- Development
- License
Features
- Drag & Drop Grid — smooth cursor-following drag with collision resolution and auto-compaction.
- Live Resize — bottom-right resize handle; surrounding cards shift out of the way in real time.
- Multi-Workspace — unlimited tabbed workspaces, each with its own independent layout.
- Independent Pop-out Layouts — pop a workspace into its own window; dragging there never affects the main window.
- Live Cross-Tab Sync — adding, removing, renaming, moving, and resizing widgets in any window instantly propagates to all open windows via
BroadcastChannel. - Pluggable Persistence — localStorage by default, or inject your own DB/API adapter.
- Content-Agnostic Cards — you own the card body; stamp any Angular component, chart, or markup inside via
ng-template. - Fully Themeable — 36 colour tokens plus font family, controllable via a typed
DashboardThemeinput or CSS custom properties. - Localizable / Customizable Text — every UI string (button labels, tooltips) is overridable via a typed
DashboardLabelsinput.
Installation
npm install @ogidor/dashboard line-awesomeAdd Line Awesome to your global styles (styles.scss or angular.json):
@import "line-awesome/dist/line-awesome/css/line-awesome.css";Quick Start
1. Import DashboardModule
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { DashboardModule } from '@ogidor/dashboard';
@NgModule({
imports: [BrowserModule, DashboardModule],
bootstrap: [AppComponent],
})
export class AppModule {}2. Add the component to your template
dynamic-dashboard fills whatever container you place it in — you control the size. Give its parent a defined height and the dashboard will fill it completely.
<!-- Full-page example -->
<div style="width: 100vw; height: 100vh;">
<dynamic-dashboard ...></dynamic-dashboard>
</div>
<!-- Embedded in a layout example -->
<div class="dashboard-container">
<dynamic-dashboard ...></dynamic-dashboard>
</div>/* your global stylesheet */
.dashboard-container {
width: 100%;
height: calc(100vh - 60px); /* e.g. subtract a top nav */
}<dynamic-dashboard
(addWidgetRequested)="onAddWidget()"
(editWidgetRequested)="onEditWidget($event)"
>
<!-- Stamp your own content inside every card -->
<ng-template dynamicGridCell let-widget="widget">
<div style="padding: 8px; color: white;">
{{ widget.data | json }}
</div>
</ng-template>
</dynamic-dashboard>3. Add and edit widgets programmatically
import { DashboardStateService, Widget } from '@ogidor/dashboard';
export class AppComponent {
constructor(private dashboard: DashboardStateService) {}
onAddWidget() {
this.dashboard.addWidget({
title: 'Revenue',
cols: 6,
rows: 4,
cardColor: '#1a1a2e', // optional — overrides the global card color
data: { metric: 'revenue' },
});
}
onEditWidget(widget: Widget) {
// open your own dialog, then patch:
this.dashboard.updateWidget(widget.id, {
title: 'Updated Title',
data: { metric: 'users' },
});
}
}API Reference
dynamic-dashboard
The main component. Handles tabs, the Add Widget button, and the grid.
Inputs
| Input | Type | Description |
|---|---|---|
| theme | DashboardTheme | Override any colour token or font. All fields optional. |
| labels | DashboardLabels | Override any UI string (button labels, tooltips). All fields optional. |
| initialLayout | string | JSON from a previous serializeLayout() call. Restores the full layout on load. Takes precedence over any layout previously saved to the persistence layer. |
Outputs
| Output | Payload | Description |
|---|---|---|
| addWidgetRequested | void | Fires when the user clicks Add Widget. Open your own dialog here. |
| editWidgetRequested | Widget | Fires when the user clicks the edit icon on a card. |
| removePageRequested | Page | Fires when the user clicks × on a tab or the remove button in the tab-switcher. Confirm and call dash.removePage(page.id). |
Content children
| Selector | Description |
|---|---|
| ng-template[dynamicGridCell] | Rendered inside every card body. Template context exposes widget: Widget. |
Methods (via @ViewChild)
@ViewChild(DashboardComponent) dash!: DashboardComponent;
// Remove a page (call from your removePageRequested handler after confirmation)
this.dash.removePage(page.id);
// Returns the current layout as a JSON string
const json = this.dash.serializeLayout();DashboardStateService
Provided by DashboardModule — scoped to each module import, not the root injector. Inject it in any component within the same module's injector tree for programmatic control.
constructor(private state: DashboardStateService) {}Methods
| Method | Signature | Description |
|---|---|---|
| addWidget | <T>(widget: Partial<Widget<T>> & { title: string }) => Widget<T> | Add a widget to the active workspace. Returns the new widget with its generated id. Provide a type parameter for a typed data field. |
| updateWidget | (id: string, patch: Partial<Omit<Widget, 'id'|'x'|'y'|'cols'|'rows'>>) => void | Update title, cardColor, or data. Use updateWidgetPosition for positional fields. |
| removeWidget | (id: string) => void | Remove a widget from the active workspace. |
| updateWidgetPosition | (pageId, widgetId, x, y, cols, rows) => void | Manually reposition or resize a widget. |
| getActivePage | () => Page \| undefined | Returns the currently active Page object. |
| addPage | (name: string) => void | Add a new workspace tab. |
| removePage | (id: string) => void | Remove a workspace tab (at least one is always kept). |
| setActivePage | (id: string) => void | Switch the active workspace by id. |
| popOutPage | (id: string) => void | Open a workspace in a separate browser window. |
| loadLayout | (config: unknown) => void | Load a layout object (parsed from serializeLayout). Validates the structure before applying; invalid or malformed input is silently ignored. |
| serializeLayout | () => string | Serialize the full layout — pages, widget metadata, and current card positions — to a JSON string. Pass the output to loadLayout to fully restore the layout. |
Observables
| Observable | Type | Description |
|---|---|---|
| pages$ | Observable<Page[]> | Emits whenever the page list changes. |
| activePageId$ | Observable<string> | Emits whenever the active workspace changes. |
dynamic-grid (Advanced)
The raw grid engine, exported for advanced use cases. dynamic-dashboard uses this internally.
<dynamic-grid
[widgets]="myWidgets"
[columns]="12"
[gap]="16"
[rowHeight]="80"
(itemChanged)="onItemChanged($event)"
(layoutChanged)="onLayoutChanged($event)"
>
<ng-template dynamicGridCell let-widget="widget">
<!-- your card here -->
</ng-template>
</dynamic-grid>Inputs
| Input | Type | Default | Description |
|---|---|---|---|
| widgets | Widget[] | [] | The widgets to render. |
| columns | number | 12 | Number of grid columns. |
| gap | number | 16 | Gap between cells in pixels. |
| rowHeight | number | 80 | Height of one grid row in pixels. |
| minItemCols | number | 1 | Minimum column span during resize. |
| minItemRows | number | 1 | Minimum row span during resize. |
Outputs
| Output | Payload | Description |
|---|---|---|
| itemChanged | Widget | Fires after a single widget is moved or resized. Payload is a shallow copy — safe to mutate. |
| layoutChanged | Widget[] | Fires after any move or resize with the full updated list. Payload is a new array of shallow copies. |
Important:
dynamic-gridnever mutates the[widgets]input array or its objects. It maintains an internal copy. To keep your state in sync, update your source array from thelayoutChangedpayload — this is the pattern required by NgRx, signals, or any immutable state approach.
// Correct — replace your array from the emitted copy
onLayoutChanged(updated: Widget[]) {
this.myWidgets = updated;
}
// Also correct for NgRx
onLayoutChanged(updated: Widget[]) {
this.store.dispatch(widgetsUpdated({ widgets: updated }));
}Public Exports
export { DashboardModule } from '@ogidor/dashboard';
export { DashboardComponent } from '@ogidor/dashboard';
export { WidgetRendererComponent } from '@ogidor/dashboard';
export { CustomGridComponent } from '@ogidor/dashboard';
export { GridCellDirective } from '@ogidor/dashboard';
export { DashboardStateService } from '@ogidor/dashboard';
export {
DashboardPersistence,
LocalStorageDashboardPersistence,
DASHBOARD_PERSISTENCE,
DASHBOARD_PERSISTENCE_CONTEXT,
PositionMap,
} from '@ogidor/dashboard';
export { Widget, Page, DashboardConfig, DashboardTheme, DashboardLabels } from '@ogidor/dashboard';Models
Widget<T>
// GridRect — the geometry base shared with the grid engine
export interface GridRect {
id: string; // auto-generated — do not set manually
x: number; // grid column, 0-based
y: number; // grid row, 0-based
cols: number; // column span (default: 4)
rows: number; // row span (default: 3)
}
export interface Widget<T = unknown> extends GridRect {
title: string; // shown in the card header
cardColor?: string; // per-card background — overrides the global widgetCardColor
data?: T; // custom payload — stored and synced, never inspected by the lib
}Provide your own data type for full type safety:
interface ChartData { metric: string; range: '7d' | '30d'; }
// addWidget infers the return type as Widget<ChartData>
const w = this.dashboard.addWidget<ChartData>({
title: 'Revenue',
data: { metric: 'revenue', range: '30d' },
});
// In your ng-template the widget is Widget<unknown> by default;
// cast inside your own component where you know T:
onEdit(widget: Widget) {
const data = widget.data as ChartData;
}Page
export interface Page {
id: string;
name: string;
widgets: Widget[];
}DashboardConfig
export interface DashboardConfig {
pages: Page[];
activePageId: string;
}DashboardTheme
export interface DashboardTheme {
// ── Layout ──
backgroundColor?: string; // outer wrapper default: #000000
panelColor?: string; // main panel bg default: #1c1c1e
panelBorderColor?: string; // panel & pop-out border default: rgba(255,255,255,0.05)
widgetCardColor?: string; // card background default: #2c2c2e
foreColor?: string; // text / labels default: #8e8e93
accentColor?: string; // active states, buttons default: #0a84ff
accentTintColor?: string; // light accent hover bg default: rgba(10,132,255,0.15)
accentTintStrongColor?: string; // strong accent active bg default: rgba(10,132,255,0.25)
dangerColor?: string; // destructive actions default: #ff453a
dangerTintColor?: string; // light danger hover bg default: rgba(255,69,58,0.22)
fontFamily?: string; // global font stack default: system-ui
// ── Header & Tabs ──
tabsContainerColor?: string; // pill container bg default: rgba(44,44,46,0.6)
tabActiveColor?: string; // active tab bg default: #3a3a3c
tabActiveTextColor?: string; // active tab text default: #ffffff
tabHoverTextColor?: string; // hovered tab text default: #e5e5ea
tabHoverBgColor?: string; // hovered tab bg default: rgba(255,255,255,0.05)
addWidgetButtonTextColor?: string; // Add Widget label default: #ffffff
addWidgetButtonIconColor?: string; // Add Widget icon colour default: #ffffff
addWidgetButtonBgColor?: string; // Add Widget button bg default: #0a84ff
tabActiveIconColor?: string; // active tab action icons default: #ffffff
tabInactiveIconColor?: string; // inactive tab action icons default: #8e8e93
overflowBtnBgColor?: string; // "+N more" button bg default: rgba(255,255,255,0.08)
overflowBtnBorderColor?: string; // "+N more" button border default: rgba(255,255,255,0.12)
// ── Tab-switcher overlay ──
overlayBgColor?: string; // dimmed backdrop default: rgba(0,0,0,0.55)
sheetBorderColor?: string; // sheet border & dividers default: rgba(255,255,255,0.08)
scrollbarColor?: string; // scrollbar thumb default: rgba(255,255,255,0.15)
switcherTitleColor?: string; // dialog "N Workspaces" text default: #ffffff
switcherCardTextColor?: string; // dialog card name text default: #ffffff
// ── Pop-out Window ──
popoutTitleColor?: string; // pop-out title text default: #ffffff
// ── Widget Card ──
widgetTitleColor?: string; // card title default: #ffffff
dragHandleColor?: string; // drag dots default: rgba(255,255,255,0.2)
widgetBorderColor?: string; // card border default: rgba(255,255,255,0.07)
widgetButtonBgColor?: string; // icon button bg default: rgba(255,255,255,0.07)
widgetButtonColor?: string; // icon button colour default: rgba(255,255,255,0.5)
// ── Grid ──
placeholderColor?: string; // drag ghost overlay default: #0a84ff
resizeHandleColor?: string; // resize icon default: rgba(255,255,255,0.25)
}Per-card colour: set
cardColoron anyWidgetto overridewidgetCardColorfor just that one card.
DashboardLabels
export interface DashboardLabels {
addWidgetButton?: string; // "Add Widget" — visible button text
addWidgetButtonTitle?: string; // "Add widget" — button tooltip
addPageButtonTitle?: string; // "New workspace" — + tab button tooltip
overflowButtonTitle?: string; // "Show all workspaces" — overflow +N tooltip
popoutButtonTitle?: string; // "Open in new window" — pop-out icon tooltip
closeTabButtonTitle?: string; // "Close" — × tab button tooltip
workspaceSingular?: string; // "Workspace" — tab-switcher title (1 item)
workspacePlural?: string; // "Workspaces" — tab-switcher title (N items)
newWorkspaceButton?: string; // "New Workspace" — switcher footer button text
dragToMoveTitle?: string; // "Drag to move" — drag handle tooltip
editButtonTitle?: string; // "Edit" — widget edit icon tooltip
removeButtonTitle?: string; // "Remove" — widget remove icon tooltip
}Theming
Theme Input
Pass a partial DashboardTheme to [theme]. Only provided fields are overridden.
theme: DashboardTheme = {
accentColor: '#6c63ff',
accentTintColor: 'rgba(108,99,255,0.15)',
accentTintStrongColor: 'rgba(108,99,255,0.25)',
tabActiveColor: '#2a2a38',
tabActiveTextColor: '#ffffff',
tabActiveIconColor: '#ffffff', // icons on the active tab
tabInactiveIconColor: '#6e6e73', // icons on inactive tabs
addWidgetButtonBgColor: '#6c63ff', // button background
addWidgetButtonIconColor: '#ffffff', // + icon inside button
widgetTitleColor: '#f0f0f0',
placeholderColor: '#6c63ff',
};<dynamic-dashboard [theme]="theme"></dynamic-dashboard>CSS Custom Properties
Every token is also available as a CSS variable on the dynamic-dashboard host element:
dynamic-dashboard {
/* ── Layout ── */
--dash-bg: #0d0d0d;
--dash-panel-bg: #161616;
--dash-panel-border: rgba(255,255,255,0.06);
--dash-card-bg: #1f1f1f;
--dash-fore-color: #a0a0a0;
--dash-accent-color: #6c63ff;
--dash-accent-tint: rgba(108,99,255,0.15);
--dash-accent-tint-strong: rgba(108,99,255,0.25);
--dash-danger-color: #e84545;
--dash-danger-tint: rgba(232,69,69,0.22);
--dash-font-family: 'Inter', sans-serif;
/* ── Header / Tabs ── */
--dash-tabs-container-color: rgba(30,30,32,0.7);
--dash-tab-active-color: #2a2a2e;
--dash-tab-active-text: #ffffff;
--dash-tab-hover-text: #d0d0d5;
--dash-tab-hover-bg: rgba(255,255,255,0.05);
--dash-add-widget-text: #ffffff;
--dash-add-widget-icon: #ffffff;
--dash-add-widget-bg: #0a84ff;
--dash-tab-active-icon: #ffffff;
--dash-tab-inactive-icon: #a0a0a0;
--dash-overflow-btn-bg: rgba(255,255,255,0.08);
--dash-overflow-btn-border: rgba(255,255,255,0.12);
/* ── Tab-switcher overlay ── */
--dash-overlay-bg: rgba(0,0,0,0.6);
--dash-sheet-border: rgba(255,255,255,0.08);
--dash-scrollbar-color: rgba(255,255,255,0.15);
--dash-switcher-title: #ffffff;
--dash-switcher-card-text: #ffffff;
/* ── Pop-out ── */
--dash-popout-title-color: #ffffff;
/* ── Widget card ── */
--dash-widget-title-color: #ffffff;
--dash-drag-handle-color: rgba(255,255,255,0.2);
--dash-widget-border-color: rgba(255,255,255,0.07);
--dash-widget-btn-bg: rgba(255,255,255,0.07);
--dash-widget-btn-color: rgba(255,255,255,0.5);
/* ── Grid ── */
--dash-placeholder-color: #6c63ff;
--dash-resize-handle-color: rgba(255,255,255,0.25);
}CSS Variable Reference
| CSS Variable | DashboardTheme field | Default |
|---|---|---|
| --dash-bg | backgroundColor | #000000 |
| --dash-panel-bg | panelColor | #1c1c1e |
| --dash-panel-border | panelBorderColor | rgba(255,255,255,0.05) |
| --dash-card-bg | widgetCardColor | #2c2c2e |
| --dash-fore-color | foreColor | #8e8e93 |
| --dash-accent-color | accentColor | #0a84ff |
| --dash-accent-tint | accentTintColor | rgba(10,132,255,0.15) |
| --dash-accent-tint-strong | accentTintStrongColor | rgba(10,132,255,0.25) |
| --dash-danger-color | dangerColor | #ff453a |
| --dash-danger-tint | dangerTintColor | rgba(255,69,58,0.22) |
| --dash-font-family | fontFamily | system-ui stack |
| --dash-tabs-container-color | tabsContainerColor | rgba(44,44,46,0.6) |
| --dash-tab-active-color | tabActiveColor | #3a3a3c |
| --dash-tab-active-text | tabActiveTextColor | #ffffff |
| --dash-tab-hover-text | tabHoverTextColor | #e5e5ea |
| --dash-tab-hover-bg | tabHoverBgColor | rgba(255,255,255,0.05) |
| --dash-add-widget-text | addWidgetButtonTextColor | #ffffff |
| --dash-add-widget-icon | addWidgetButtonIconColor | #ffffff |
| --dash-add-widget-bg | addWidgetButtonBgColor | #0a84ff |
| --dash-tab-active-icon | tabActiveIconColor | #ffffff |
| --dash-tab-inactive-icon | tabInactiveIconColor | #8e8e93 |
| --dash-overflow-btn-bg | overflowBtnBgColor | rgba(255,255,255,0.08) |
| --dash-overflow-btn-border | overflowBtnBorderColor | rgba(255,255,255,0.12) |
| --dash-overlay-bg | overlayBgColor | rgba(0,0,0,0.55) |
| --dash-sheet-border | sheetBorderColor | rgba(255,255,255,0.08) |
| --dash-scrollbar-color | scrollbarColor | rgba(255,255,255,0.15) |
| --dash-switcher-title | switcherTitleColor | #ffffff |
| --dash-switcher-card-text | switcherCardTextColor | #ffffff |
| --dash-popout-title-color | popoutTitleColor | #ffffff |
| --dash-widget-title-color | widgetTitleColor | #ffffff |
| --dash-drag-handle-color | dragHandleColor | rgba(255,255,255,0.2) |
| --dash-widget-border-color | widgetBorderColor | rgba(255,255,255,0.07) |
| --dash-widget-btn-bg | widgetButtonBgColor | rgba(255,255,255,0.07) |
| --dash-widget-btn-color | widgetButtonColor | rgba(255,255,255,0.5) |
| --dash-placeholder-color | placeholderColor | #0a84ff |
| --dash-resize-handle-color | resizeHandleColor | rgba(255,255,255,0.25) |
Labels & i18n
Every visible string in the dashboard UI can be replaced via the [labels] input. All properties are optional — unset properties fall back to their English defaults.
Basic usage
<dynamic-dashboard [labels]="myLabels"></dynamic-dashboard>import { DashboardLabels } from '@ogidor/dashboard';
myLabels: DashboardLabels = {
addWidgetButton: 'New Widget',
workspaceSingular: 'Page',
workspacePlural: 'Pages',
newWorkspaceButton: 'New Page',
};Full reference
| Property | Default | Where it appears |
|---|---|---|
| addWidgetButton | 'Add Widget' | Visible text on the top-right button |
| addWidgetButtonTitle | 'Add widget' | Tooltip on that button |
| addPageButtonTitle | 'New workspace' | Tooltip on the + icon in the tab bar |
| overflowButtonTitle | 'Show all workspaces' | Tooltip on the overflow +N button |
| popoutButtonTitle | 'Open in new window' | Tooltip on the pop-out arrow icon |
| closeTabButtonTitle | 'Close' | Tooltip on the × close icon |
| workspaceSingular | 'Workspace' | Tab-switcher dialog title when there is 1 workspace |
| workspacePlural | 'Workspaces' | Tab-switcher dialog title when there are N workspaces |
| newWorkspaceButton | 'New Workspace' | Button text in the tab-switcher footer |
| dragToMoveTitle | 'Drag to move' | Tooltip on the widget drag handle |
| editButtonTitle | 'Edit' | Tooltip on the widget edit icon |
| removeButtonTitle | 'Remove' | Tooltip on the widget remove icon |
Pop-out Windows
Every workspace tab shows a pop-out button (arrow icon, visible on hover). Clicking it opens that workspace in a new browser window.
The pop-out window:
- Shows only that workspace — no tabs, no Add Widget button, just the grid.
- Has its own independent layout — moving cards there never affects the main window.
- Receives structural changes (add/remove widget, metadata edits) from all other windows in real time.
Sync behaviour
| State | Synced across windows? | Stored in |
|---|---|---|
| Page list (add / remove / rename) | Yes | ogidor_shared |
| Widget list (add / remove) | Yes | ogidor_shared |
| Widget metadata (title, cardColor, data) | Yes | ogidor_shared |
| Card positions and sizes | Yes — synced in real time | ogidor_positions_<windowId> |
Note:
serializeLayout()captures the full layout including current card positions.loadLayout()restores them in full. The internal persistence layer stores positions per window so that pop-out layouts survive a page reload independently — the publicserializeLayout/loadLayoutpair is a complete backup/restore contract.
To pop out programmatically:
this.state.popOutPage(pageId);Persisting Layouts
By default, state is saved to localStorage automatically on every change.
| Key | Contents |
|---|---|
| ogidor_shared | Page list and widget metadata for the default dashboard context. |
| ogidor_positions_main | Grid positions for the main window (default context). |
| ogidor_positions_<pageId> | Grid positions for each pop-out window (default context). |
If you omit DASHBOARD_PERSISTENCE_CONTEXT, the dashboard falls back to the id 'default'. Always provide an explicit dashboardId — relying on the fallback means different deployment paths (e.g. /app/v1/ vs /app/v2/, or the same app behind a reverse proxy) will share the same storage key and silently overwrite each other's layouts.
// Recommended: always name your dashboard
{ provide: DASHBOARD_PERSISTENCE_CONTEXT, useValue: { dashboardId: 'my-app-dashboard' } }For multi-tenant scenarios, supply a per-user or per-tenant id at runtime; keys are namespaced automatically in the default localStorage adapter.
Use a database/API instead of localStorage
Provide your own implementation of DashboardPersistence and override the DASHBOARD_PERSISTENCE token in the same module that imports DashboardModule.
Scoping note:
DashboardStateService,LocalStorageDashboardPersistence, andDashboardPersistenceFacadeare all provided byDashboardModule— not at root. This means each module that importsDashboardModulegets its own isolated service instances. Two lazy-loaded routes that each importDashboardModulewill have completely independent dashboard state. OverrideDASHBOARD_PERSISTENCEin that same module'sprovidersto apply your custom adapter to that instance.
import { Injectable, NgModule } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
DashboardConfig,
DashboardModule,
DashboardPersistence,
DASHBOARD_PERSISTENCE,
DASHBOARD_PERSISTENCE_CONTEXT,
PositionMap,
} from '@ogidor/dashboard';
@Injectable()
export class HttpDashboardPersistence extends DashboardPersistence {
constructor(private http: HttpClient) { super(); }
loadShared(dashboardId: string) {
return this.http
.get<DashboardConfig | null>(`/api/dashboards/${encodeURIComponent(dashboardId)}/shared`)
.toPromise();
}
saveShared(dashboardId: string, shared: DashboardConfig) {
return this.http
.put<void>(`/api/dashboards/${encodeURIComponent(dashboardId)}/shared`, shared)
.toPromise()
.then(() => undefined);
}
loadPositions(dashboardId: string, windowId: string) {
return this.http
.get<PositionMap | null>(`/api/dashboards/${encodeURIComponent(dashboardId)}/positions/${encodeURIComponent(windowId)}`)
.toPromise();
}
savePositions(dashboardId: string, windowId: string, positions: PositionMap) {
return this.http
.put<void>(`/api/dashboards/${encodeURIComponent(dashboardId)}/positions/${encodeURIComponent(windowId)}`, positions)
.toPromise()
.then(() => undefined);
}
}
@NgModule({
imports: [DashboardModule],
providers: [
{ provide: DASHBOARD_PERSISTENCE, useClass: HttpDashboardPersistence },
{ provide: DASHBOARD_PERSISTENCE_CONTEXT, useValue: { dashboardId: 'customer-123' } },
],
})
export class AppModule {}Recommended backend endpoints:
GET /api/dashboards/:dashboardId/sharedPUT /api/dashboards/:dashboardId/sharedGET /api/dashboards/:dashboardId/positions/:windowIdPUT /api/dashboards/:dashboardId/positions/:windowId
Migration tip
If users already have data in localStorage, run a one-time migration in your app:
- Read
ogidor_sharedandogidor_positions_*from localStorage. - Upload them to your backend using the new API.
- Set a migration flag (for example
ogidor_migrated=true). - Use your HTTP persistence adapter as the source of truth from then on.
SSR / Angular Universal
@ogidor/dashboard is safe to render server-side. All browser-only APIs (localStorage, BroadcastChannel, window, window.open) are guarded with Angular's isPlatformBrowser and will silently no-op on the server:
| API | SSR behaviour |
|---|---|
| localStorage reads | Returns null — no persisted state is loaded on the server. |
| localStorage writes | Skipped silently. |
| BroadcastChannel | Not created — cross-tab sync is disabled. |
| window.location.hash | Treated as empty — pop-out mode is never activated. |
| window.open (popOutPage) | No-op — the call returns immediately. |
No extra configuration is required. The dashboard hydrates fully once the page reaches the browser.
Full Integration Example
import { Component, ViewChild } from '@angular/core';
import {
DashboardComponent,
DashboardStateService,
DashboardTheme,
DashboardLabels,
Page,
Widget,
} from '@ogidor/dashboard';
@Component({
selector: 'app-root',
template: `
<dynamic-dashboard
[theme]="theme"
[labels]="labels"
(addWidgetRequested)="openAddWidgetDialog()"
(editWidgetRequested)="openEditWidgetDialog($event)"
(removePageRequested)="openRemovePageDialog($event)"
>
<ng-template dynamicGridCell let-widget="widget">
<my-chart [config]="widget.data"></my-chart>
</ng-template>
</dynamic-dashboard>
<my-add-widget-dialog
*ngIf="addWidgetOpen"
(confirmed)="onAddWidgetConfirmed($event)"
(cancelled)="addWidgetOpen = false"
></my-add-widget-dialog>
<my-edit-widget-dialog
*ngIf="editTarget"
[widget]="editTarget"
(confirmed)="onEditWidgetConfirmed($event)"
(cancelled)="editTarget = null"
></my-edit-widget-dialog>
<my-confirm-dialog
*ngIf="removePageTarget"
[message]="'Remove workspace ' + removePageTarget.name + '?'"
(confirmed)="onRemovePageConfirmed()"
(cancelled)="removePageTarget = null"
></my-confirm-dialog>
`,
})
export class AppComponent {
@ViewChild(DashboardComponent) dash!: DashboardComponent;
theme: DashboardTheme = {
accentColor: '#6c63ff',
tabActiveColor: '#2a2a38',
widgetTitleColor: '#f0f0f0',
};
labels: DashboardLabels = {
addWidgetButton: 'New Widget',
workspaceSingular: 'Page',
workspacePlural: 'Pages',
newWorkspaceButton: 'New Page',
};
addWidgetOpen = false;
editTarget: Widget | null = null;
removePageTarget: Page | null = null;
constructor(private state: DashboardStateService) {}
openAddWidgetDialog() { this.addWidgetOpen = true; }
openEditWidgetDialog(w: Widget) { this.editTarget = w; }
openRemovePageDialog(p: Page) { this.removePageTarget = p; }
onAddWidgetConfirmed(payload: Partial<Widget> & { title: string }) {
this.state.addWidget(payload);
this.addWidgetOpen = false;
}
onEditWidgetConfirmed(patch: Partial<Widget>) {
this.state.updateWidget(this.editTarget!.id, patch);
this.editTarget = null;
}
onRemovePageConfirmed() {
this.dash.removePage(this.removePageTarget!.id);
this.removePageTarget = null;
}
saveLayout() {
return this.dash.serializeLayout();
}
}Development
# Install dependencies
npm install
# Run the test suite
npm test
# Build the library
npm run build:lib
# Run the local demo app
npm startLicense
MIT
