diff --git a/projects/ngx-dashboard-widgets/package.json b/projects/ngx-dashboard-widgets/package.json index 8265896..92bce4e 100644 --- a/projects/ngx-dashboard-widgets/package.json +++ b/projects/ngx-dashboard-widgets/package.json @@ -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", diff --git a/projects/ngx-dashboard/package.json b/projects/ngx-dashboard/package.json index 080c1e6..962b9c4 100644 --- a/projects/ngx-dashboard/package.json +++ b/projects/ngx-dashboard/package.json @@ -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", diff --git a/projects/ngx-dashboard/src/lib/services/__tests__/dashboard.service.spec.ts b/projects/ngx-dashboard/src/lib/services/__tests__/dashboard.service.spec.ts new file mode 100644 index 0000000..7b7559c --- /dev/null +++ b/projects/ngx-dashboard/src/lib/services/__tests__/dashboard.service.spec.ts @@ -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 { + private readonly state = signal({ + 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: '', + }; +} + +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([ + ['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([ + ['test-lazy-widget', { theme: 'dark', fontSize: 24 }], + ]) + ); + + // Second load buffers a different value before the widget ever registers + service.restoreSharedStates( + new Map([ + ['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([ + ['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 }); + }); +}); diff --git a/projects/ngx-dashboard/src/lib/services/dashboard.service.ts b/projects/ngx-dashboard/src/lib/services/dashboard.service.ts index 5ffa0a3..051b2cc 100644 --- a/projects/ngx-dashboard/src/lib/services/dashboard.service.ts +++ b/projects/ngx-dashboard/src/lib/services/dashboard.service.ts @@ -15,6 +15,7 @@ export class DashboardService { readonly #widgetTypes = signal([]); readonly #widgetFactoryMap = new Map(); readonly #sharedStateProviders = new Map(); + readonly #pendingSharedStates = new Map(); readonly #unknownWidgetFactory = createFactoryFromComponent(UnknownWidgetComponent); readonly widgetTypes = this.#widgetTypes.asReadonly(); // make the widget list available as a readonly signal @@ -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]); @@ -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); } } }