diff --git a/packages/core/src/render3/instructions/defer_events.ts b/packages/core/src/render3/instructions/defer_events.ts index 4df351f10afb9..9e271d9b6d53d 100644 --- a/packages/core/src/render3/instructions/defer_events.ts +++ b/packages/core/src/render3/instructions/defer_events.ts @@ -176,6 +176,11 @@ class DeferIntersectionManager { entry.callbacks.add(callback); return () => { + // It's possible that a different cleanup callback fully removed this element already. + if (!this.viewportTriggers.has(trigger)) { + return; + } + entry!.callbacks.delete(callback); if (entry!.callbacks.size === 0) { diff --git a/packages/core/test/acceptance/defer_spec.ts b/packages/core/test/acceptance/defer_spec.ts index 04f7d0d4b9167..5c1f96a741d74 100644 --- a/packages/core/test/acceptance/defer_spec.ts +++ b/packages/core/test/acceptance/defer_spec.ts @@ -3610,5 +3610,44 @@ describe('@defer', () => { expect(loadingFnInvokedTimes).toBe(1); })); + + it('should load deferred content in a loop', fakeAsync(() => { + @Component({ + standalone: true, + template: ` + @for (item of items; track item) { + @defer (on viewport) {d{{item}} } + @placeholder {} + } + ` + }) + class MyCmp { + items = [1, 2, 3, 4, 5, 6]; + } + + const fixture = TestBed.createComponent(MyCmp); + fixture.detectChanges(); + const buttons = Array.from(fixture.nativeElement.querySelectorAll('button')); + const items = fixture.componentInstance.items; + + // None of the blocks are loaded yet. + expect(fixture.nativeElement.textContent.trim()).toBe('p1 p2 p3 p4 p5 p6'); + + // First half of the blocks is loaded. + for (let i = 0; i < items.length / 2; i++) { + MockIntersectionObserver.invokeCallbacksForElement(buttons[i], true); + fixture.detectChanges(); + flush(); + } + expect(fixture.nativeElement.textContent.trim()).toBe('d1 d2 d3 p4 p5 p6'); + + // Second half of the blocks is loaded. + for (let i = items.length / 2; i < items.length; i++) { + MockIntersectionObserver.invokeCallbacksForElement(buttons[i], true); + fixture.detectChanges(); + flush(); + } + expect(fixture.nativeElement.textContent.trim()).toBe('d1 d2 d3 d4 d5 d6'); + })); }); });