Skip to content

Commit

Permalink
fix(core): viewport trigger deregistering callbacks multiple times
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto committed Oct 10, 2023
1 parent 229331e commit 0efeb20
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/core/src/render3/instructions/defer_events.ts
Expand Up @@ -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) {
Expand Down
39 changes: 39 additions & 0 deletions packages/core/test/acceptance/defer_spec.ts
Expand Up @@ -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 {<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');
}));
});
});

0 comments on commit 0efeb20

Please sign in to comment.