Angular  

Building Dynamic Dashboard Widgets with Persistence Store in Angular

A complete practical guide for beginner to senior developers

Modern web applications often require dashboards that users can customise based on their needs. A sales executive may want to see leads and revenue widgets. A project manager may want status charts and team availability widgets. A developer may want logs, deployments and metrics. Every user’s needs are different. Because of this, dynamic dashboards with widget personalisation and persistence have become a common requirement in enterprise applications.

In this article, we will build a practical Angular-based solution for a Dynamic Dashboard Widget System that allows users to:

  1. Add new widgets

  2. Remove widgets

  3. Rearrange widgets (drag and drop)

  4. Resize widgets (optional)

  5. Persist the configuration so it remains the same after page reload

  6. Load widgets dynamically from a server (recommended in real applications)

We will approach this topic with production-grade patterns, clean architecture, and simple but effective Angular techniques. The goal is to make the implementation understandable for beginners while also valuable for senior developers looking for scalable patterns.

1. Understanding the Problem

A dynamic dashboard is essentially a canvas that loads small independent components called widgets. Each widget is self-contained: it has its own UI, its own API calls, and its own internal logic.

A good widget system must handle:

1. Widget Registry

A place where all available widgets are listed (for example, Sales Chart, Notifications, Tasks, Analytics, etc.).

2. Widget Instances

Actual widgets that appear on the user’s dashboard.
Example:
A user may add two Chart widgets but only one Notification widget.

3. Persistence Layer

This is where the user’s dashboard layout is stored:

  • Local Storage (basic)

  • IndexedDB (large data)

  • Backend DB (real-world recommended)

4. Dynamic Component Loading

Widgets must be loaded dynamically using Angular’s component factory or Angular’s newer standalone component APIs.

5. State Management

A predictable state flow is required when widgets are added, removed or moved.
Options include:

  • RxJS services (simple approach)

  • NgRx store (enterprise)

  • Component store (balanced option)

In this article, we will use a clean RxJS-based service to keep the architecture light and easy to follow.

2. Project Setup and Structure

A well-structured Angular project helps keep the dashboard scalable. Below is a recommended folder structure:

src/app/
  core/
    services/
      persistence.service.ts
      widget-registry.service.ts
      dashboard-state.service.ts
  shared/
    models/
      widget-config.model.ts
  widgets/
    sales-chart/
      sales-chart.component.ts
    tasks/
      tasks-widget.component.ts
    notifications/
      notifications-widget.component.ts
  dashboard/
    dashboard.module.ts
    dashboard.component.ts
    widget-host.directive.ts

Key Modules

  1. Core Module
    Contains application-wide singleton services.

  2. Widgets Module
    Contains all actual widget components.

  3. Dashboard Module
    Contains the dynamic dashboard UI, widget host container and logic.

3. Creating a Common Widget Configuration Model

All widgets must follow a common structure.
Create a model:

export interface WidgetConfig {
  id: string;              // unique widget instance id
  type: string;            // widget type (chart, tasks, etc.)
  title: string;           // display name
  position: {              // for drag & drop
    x: number;
    y: number;
  };
  size: {                  // widget size (optional)
    width: number;
    height: number;
  };
  data?: any;              // optional configuration data
}

This structure will be saved, restored, and used to render widgets dynamically.

4. Widget Registry Service

This service knows all widget types available in the system.

@Injectable({ providedIn: 'root' })
export class WidgetRegistryService {

  private registry = new Map<string, Type<any>>();

  register(type: string, component: Type<any>) {
    this.registry.set(type, component);
  }

  getComponent(type: string): Type<any> | undefined {
    return this.registry.get(type);
  }

  getAll(): string[] {
    return Array.from(this.registry.keys());
  }
}

Registering Widgets

You can register widgets inside AppModule:

constructor(widgetRegistry: WidgetRegistryService) {
  widgetRegistry.register('sales-chart', SalesChartComponent);
  widgetRegistry.register('tasks', TasksWidgetComponent);
  widgetRegistry.register('notifications', NotificationsWidgetComponent);
}

This makes widgets discoverable across your application.

5. Persistence Service

This service stores the dashboard configuration.

Basic implementation using Local Storage:

@Injectable({ providedIn: 'root' })
export class PersistenceService {

  private readonly KEY = 'dashboard-widgets';

  save(configs: WidgetConfig[]) {
    localStorage.setItem(this.KEY, JSON.stringify(configs));
  }

  load(): WidgetConfig[] {
    const saved = localStorage.getItem(this.KEY);
    return saved ? JSON.parse(saved) : [];
  }
}

In real-world systems, this service would communicate with an API:

saveOnServer(configs: WidgetConfig[]) {
  return this.http.post('/api/dashboard', configs);
}

6. Dashboard State Service

This service manages all widget instances and exposes them as an observable so the UI can reactively update.

@Injectable({ providedIn: 'root' })
export class DashboardStateService {

  private widgetsSubject = new BehaviorSubject<WidgetConfig[]>([]);
  widgets$ = this.widgetsSubject.asObservable();

  constructor(private persistence: PersistenceService) {
    const saved = this.persistence.load();
    this.widgetsSubject.next(saved);
  }

  addWidget(config: WidgetConfig) {
    const current = this.widgetsSubject.getValue();
    const updated = [...current, config];
    this.widgetsSubject.next(updated);
    this.persistence.save(updated);
  }

  removeWidget(id: string) {
    const updated = this.widgetsSubject.getValue().filter(w => w.id !== id);
    this.widgetsSubject.next(updated);
    this.persistence.save(updated);
  }

  updateWidget(config: WidgetConfig) {
    const updated = this.widgetsSubject.getValue().map(w =>
      w.id === config.id ? config : w
    );
    this.widgetsSubject.next(updated);
    this.persistence.save(updated);
  }
}

Why RxJS?

RxJS streams keep the dashboard reactive.
Components only subscribe to widgets$ and the UI updates automatically.

7. Creating the Widget Host Directive

This directive will serve as a placeholder for dynamic components.

@Directive({
  selector: '[widgetHost]'
})
export class WidgetHostDirective {
  constructor(public viewContainerRef: ViewContainerRef) {}
}

Angular will use this placeholder to render widgets dynamically.

8. Dashboard Component Template

Here is the container UI:

<div class="dashboard">
  <div *ngFor="let widget of widgets$ | async">
    <div class="widget-box">
      <h3>{{ widget.title }}</h3>

      <ng-template widgetHost></ng-template>
    </div>
  </div>
</div>

<button (click)="addNewWidget()">Add Widget</button>

Later, you can add grid layout systems like Angular CDK DragDrop or Gridster.js.

9. Loading Widgets Dynamically in Dashboard Component

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html'
})
export class DashboardComponent implements AfterViewInit {

  widgets$ = this.state.widgets$;

  @ViewChildren(WidgetHostDirective) 
  hosts!: QueryList<WidgetHostDirective>;

  constructor(
    private state: DashboardStateService,
    private registry: WidgetRegistryService,
    private resolver: ComponentFactoryResolver
  ) {}

  ngAfterViewInit() {
    this.widgets$.subscribe(() => {
      setTimeout(() => this.loadWidgets());
    });
  }

  loadWidgets() {
    const hostsArray = this.hosts.toArray();
    const widgets = this.state.widgetsSubject.getValue();

    widgets.forEach((config, index) => {
      const host = hostsArray[index];
      const widgetComponent = this.registry.getComponent(config.type);

      if (!widgetComponent) return;

      const factory = this.resolver.resolveComponentFactory(widgetComponent);
      const view = host.viewContainerRef;

      view.clear();
      const componentRef = view.createComponent(factory);

      componentRef.instance.config = config;
    });
  }

  addNewWidget() {
    const widget: WidgetConfig = {
      id: crypto.randomUUID(),
      type: 'sales-chart',
      title: 'Sales Chart',
      position: { x: 0, y: 0 },
      size: { width: 300, height: 250 }
    };

    this.state.addWidget(widget);
  }
}

Key Notes

  • A new widget is dynamically inserted inside the host container.

  • Each widget receives its configuration through componentRef.instance.config.

10. Building a Sample Widget (Sales Chart)

@Component({
  selector: 'app-sales-chart',
  template: `
    <div>
      <p>Sales Chart Widget</p>
    </div>
  `
})
export class SalesChartComponent implements OnInit {
  @Input() config!: WidgetConfig;

  ngOnInit() {
    console.log('Loaded widget config', this.config);
  }
}

In real applications, widgets may fetch data using services, charts, HTTP calls, etc.

11. Adding Drag and Drop Support (Optional but Recommended)

Angular CDK provides a very clean API:

Install CDK

npm install @angular/cdk

Add DragDropModule

imports: [
  DragDropModule
]

Update Template

<div
  cdkDrag
  class="widget-box"
  *ngFor="let widget of widgets$ | async"
  (cdkDragEnded)="onDragEnd($event, widget)"
>

Update Position After Drag

onDragEnd(event: CdkDragEnd, widget: WidgetConfig) {
  const { x, y } = event.source.getFreeDragPosition();

  const updated = {
    ...widget,
    position: { x, y }
  };

  this.state.updateWidget(updated);
}

This enables persistent drag and drop layout.

12. Adding Widget Manager (Selection Panel)

Create a simple UI to let users add widgets:

<select [(ngModel)]="selectedType">
  <option *ngFor="let widgetType of widgetTypes" [value]="widgetType">
    {{ widgetType }}
  </option>
</select>

<button (click)="addWidget()">Add</button>

In component:

widgetTypes: string[] = [];
selectedType = '';

constructor(private registry: WidgetRegistryService) {}

ngOnInit() {
  this.widgetTypes = this.registry.getAll();
  this.selectedType = this.widgetTypes[0];
}

addWidget() {
  const widget: WidgetConfig = {
    id: crypto.randomUUID(),
    type: this.selectedType,
    title: this.selectedType,
    position: { x: 0, y: 0 },
    size: { width: 300, height: 250 }
  };

  this.state.addWidget(widget);
}

13. Production Best Practices

1. Do not load widget components into AppModule

Keep them in a dedicated module (like WidgetsModule).

2. Use lazy loading for heavy widgets

Charts and analytics widgets often have large libraries.
Load them only when required.

3. Use backend persistence

Local Storage is fine for demos, but enterprise dashboards require server storage.

4. Use Angular ComponentStore or NgRx for large dashboards

This keeps the state predictable and easier to debug.

5. Use a grid system

Gridster, Angular CDK DropList, or custom CSS grid helps create smoother layout management.

6. Cache widget data

Some widgets make heavy API calls. Cache responses using services or state-management solutions.

7. Add permission control

Show or hide widgets based on user roles.

14. Putting It All Together

By now, you have:

  • A widget registry

  • A persistence layer

  • A reactive dashboard state

  • A dynamic widget loader

  • A widget selection panel

  • Optional drag and drop support

This gives you a complete production-ready structure to build scalable dynamic dashboards in Angular.

This architecture supports:

  • Enterprise analytics dashboards

  • Admin consoles

  • Monitoring dashboards

  • SaaS configurable home screens

  • Personalised BI dashboards

Conclusion

Building a dynamic dashboard widget system in Angular is not just about adding or removing components. It requires a well-thought-out structure involving dynamic component loading, a centralised registry, a persistence layer, a predictable state system and smooth UI interaction with features like drag and drop. With the patterns shown in this article, you can build a scalable, maintainable dashboard that is ready for real-world production applications.

If you want the full codebase structure, GitHub-style folder layout, or advanced features like animation, server-side rendering support or widget marketplace integration, feel free to ask.