npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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

  1. Features
  2. Installation
  3. Quick Start
  4. API Reference
  5. Models
  6. Theming
  7. Labels & i18n
  8. Pop-out Windows
  9. Persisting Layouts
  10. SSR / Angular Universal
  11. Full Integration Example
  12. Development
  13. 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 DashboardTheme input or CSS custom properties.
  • Localizable / Customizable Text — every UI string (button labels, tooltips) is overridable via a typed DashboardLabels input.

Installation

npm install @ogidor/dashboard line-awesome

Add 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'&#124;'x'&#124;'y'&#124;'cols'&#124;'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-grid never mutates the [widgets] input array or its objects. It maintains an internal copy. To keep your state in sync, update your source array from the layoutChanged payload — 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 cardColor on any Widget to override widgetCardColor for 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 public serializeLayout / loadLayout pair 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, and DashboardPersistenceFacade are all provided by DashboardModule — not at root. This means each module that imports DashboardModule gets its own isolated service instances. Two lazy-loaded routes that each import DashboardModule will have completely independent dashboard state. Override DASHBOARD_PERSISTENCE in that same module's providers to 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/shared
  • PUT /api/dashboards/:dashboardId/shared
  • GET /api/dashboards/:dashboardId/positions/:windowId
  • PUT /api/dashboards/:dashboardId/positions/:windowId

Migration tip

If users already have data in localStorage, run a one-time migration in your app:

  1. Read ogidor_shared and ogidor_positions_* from localStorage.
  2. Upload them to your backend using the new API.
  3. Set a migration flag (for example ogidor_migrated=true).
  4. 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 start

License

MIT