Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion projects/ngx-dashboard-widgets/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dragonworks/ngx-dashboard-widgets",
"version": "21.1.0",
"version": "21.1.1",
"description": "Widget collection for ngx-dashboard with Material Design 3 compliance including arrow, label, clock widgets and responsive text directive",
"peerDependencies": {
"@angular/common": "^21.0.0",
Expand Down
2 changes: 1 addition & 1 deletion projects/ngx-dashboard/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dragonworks/ngx-dashboard",
"version": "21.1.0",
"version": "21.1.1",
"description": "Angular library for building drag-and-drop grid dashboards with resizable cells and customizable widgets",
"peerDependencies": {
"@angular/common": "^21.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { TestBed } from '@angular/core/testing';
import { Injectable, signal } from '@angular/core';
import { DashboardService } from '../dashboard.service';
import {
WidgetComponentClass,
WidgetSharedStateProvider,
} from '../../models';

interface TestConfig {
theme: string;
fontSize: number;
}

@Injectable({ providedIn: 'root' })
class TestSharedState implements WidgetSharedStateProvider<TestConfig> {
private readonly state = signal<TestConfig>({
theme: 'default',
fontSize: 12,
});

readonly config = this.state.asReadonly();

getSharedState(): TestConfig {
return this.state();
}

setSharedState(state: TestConfig): void {
this.state.set(state);
}
}

class TestWidgetComponent {
static metadata = {
widgetTypeid: 'test-lazy-widget',
name: 'Test Lazy Widget',
description: 'Widget registered after loadDashboard',
svgIcon: '<svg></svg>',
};
}

describe('DashboardService - shared state restoration with late registration', () => {
let service: DashboardService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DashboardService);
});

it('applies shared state when provider is registered BEFORE restore (baseline)', () => {
TestBed.runInInjectionContext(() => {
service.registerWidgetType(
TestWidgetComponent as unknown as WidgetComponentClass,
TestSharedState
);
});

const statesMap = new Map<string, unknown>([
['test-lazy-widget', { theme: 'dark', fontSize: 24 }],
]);
service.restoreSharedStates(statesMap);

const provider = service.getSharedStateProvider(
'test-lazy-widget'
) as TestSharedState;
expect(provider.getSharedState()).toEqual({ theme: 'dark', fontSize: 24 });
});

it('later restoreSharedStates call overwrites an earlier buffered value for an unregistered type', () => {
// First load buffers one value
service.restoreSharedStates(
new Map<string, unknown>([
['test-lazy-widget', { theme: 'dark', fontSize: 24 }],
])
);

// Second load buffers a different value before the widget ever registers
service.restoreSharedStates(
new Map<string, unknown>([
['test-lazy-widget', { theme: 'light', fontSize: 16 }],
])
);

TestBed.runInInjectionContext(() => {
service.registerWidgetType(
TestWidgetComponent as unknown as WidgetComponentClass,
TestSharedState
);
});

const provider = service.getSharedStateProvider(
'test-lazy-widget'
) as TestSharedState;
expect(provider.getSharedState()).toEqual({ theme: 'light', fontSize: 16 });
});

it('applies shared state when provider is registered AFTER restore (lazy-loaded module)', () => {
// Simulate loadDashboard happening before the lazy module registers its widget type
const statesMap = new Map<string, unknown>([
['test-lazy-widget', { theme: 'dark', fontSize: 24 }],
]);
service.restoreSharedStates(statesMap);

// Later, the lazy module loads and registers the widget + provider
TestBed.runInInjectionContext(() => {
service.registerWidgetType(
TestWidgetComponent as unknown as WidgetComponentClass,
TestSharedState
);
});

const provider = service.getSharedStateProvider(
'test-lazy-widget'
) as TestSharedState;
expect(provider.getSharedState()).toEqual({ theme: 'dark', fontSize: 24 });
});
});
14 changes: 14 additions & 0 deletions projects/ngx-dashboard/src/lib/services/dashboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class DashboardService {
readonly #widgetTypes = signal<WidgetComponentClass[]>([]);
readonly #widgetFactoryMap = new Map<string, WidgetFactory>();
readonly #sharedStateProviders = new Map<string, WidgetSharedStateProvider>();
readonly #pendingSharedStates = new Map<string, unknown>();
readonly #unknownWidgetFactory = createFactoryFromComponent(UnknownWidgetComponent);
readonly widgetTypes = this.#widgetTypes.asReadonly(); // make the widget list available as a readonly signal

Expand Down Expand Up @@ -44,6 +45,14 @@ export class DashboardService {
if (sharedStateProvider) {
const provider = this.#resolveProvider(sharedStateProvider);
this.#sharedStateProviders.set(widgetTypeid, provider);

// Drain any buffered shared state from a prior loadDashboard that ran
// before this widget type was registered (e.g. lazy-loaded modules).
if (this.#pendingSharedStates.has(widgetTypeid)) {
const registered = this.#sharedStateProviders.get(widgetTypeid)!;
registered.setSharedState(this.#pendingSharedStates.get(widgetTypeid));
this.#pendingSharedStates.delete(widgetTypeid);
}
}

this.#widgetTypes.set([...this.#widgetTypes(), widget]);
Expand Down Expand Up @@ -128,6 +137,11 @@ export class DashboardService {
const provider = this.#sharedStateProviders.get(widgetTypeid);
if (provider) {
provider.setSharedState(state);
this.#pendingSharedStates.delete(widgetTypeid);
} else {
// Buffer for a widget type that hasn't been registered yet. Replaces
// any prior buffered value so a second loadDashboard wins.
this.#pendingSharedStates.set(widgetTypeid, state);
}
}
}
Expand Down
Loading