fix(core): viewport trigger deregistering callbacks multiple times (#52115)

Adds a check to the viewport cleanup function to prevent it from re-processing elements that have been fully cleaned up, because it can lead to the `IntersectionObserver` being destroyed even though there are still pending triggers. This can happen, because we have cleanup callbacks both for the block is loaded, but also when the placeholder view is destroyed.

Fixes #52113.

PR Close #52115
This commit is contained in:
Kristiyan Kostadinov
2023-10-10 11:12:08 +02:00
committed by Andrew Scott
parent 861ce3a7c5
commit d5dad3eb4c
2 changed files with 44 additions and 0 deletions
@@ -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) {
@@ -3642,5 +3642,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 {<button>p{{item}} </button>}
}
`
})
class MyCmp {
items = [1, 2, 3, 4, 5, 6];
}
const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();
const buttons = Array.from<Element>(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');
}));
});
});