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:
Add new widgets
Remove widgets
Rearrange widgets (drag and drop)
Resize widgets (optional)
Persist the configuration so it remains the same after page reload
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:
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:
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
Core Module
Contains application-wide singleton services.
Widgets Module
Contains all actual widget components.
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
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:
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.