Skip to content

Commit 4f69d62

Browse files
crisbetoalxhub
authored andcommitted
fix(core): deferred blocks not removing content immediately when animations are enabled (#51971)
Fixes an issue where if animations are enabled, deferred blocks don't remove their placeholder blocks immediately from the DOM. The problem is that we register the event handlers in `afterRender` which runs outside the zone, but the logic that removes the DOM nodes during animations is tied to change detection. These changes resolve the issue by binding the listeners inside the zone. This was the intention from the beginning, I just forgot that `afterRender` runs outside the zone. Fixes #51970. PR Close #51971
1 parent 31edf79 commit 4f69d62

File tree

3 files changed

+72
-7
lines changed

3 files changed

+72
-7
lines changed

packages/core/src/render3/instructions/defer.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import {InjectionToken, Injector, ɵɵdefineInjectable} from '../../di';
10-
import {inject} from '../../di/injector_compatibility';
1110
import {findMatchingDehydratedView} from '../../hydration/views';
1211
import {populateDehydratedViewsInLContainer} from '../../linker/view_container_ref';
1312
import {assertDefined, assertElement, assertEqual, throwError} from '../../util/assert';

packages/core/src/render3/instructions/defer_events.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ class DeferEventEntry {
3939
* Registers an interaction trigger.
4040
* @param trigger Element that is the trigger.
4141
* @param callback Callback to be invoked when the trigger is interacted with.
42+
* @param injector Injector that can be used by the trigger to resolve DI tokens.
4243
*/
43-
export function onInteraction(trigger: Element, callback: VoidFunction) {
44+
export function onInteraction(
45+
trigger: Element, callback: VoidFunction, injector: Injector): VoidFunction {
4446
let entry = interactionTriggers.get(trigger);
4547

4648
// If this is the first entry for this element, add the listeners.
@@ -59,9 +61,13 @@ export function onInteraction(trigger: Element, callback: VoidFunction) {
5961
entry = new DeferEventEntry();
6062
interactionTriggers.set(trigger, entry);
6163

62-
for (const name of interactionEventNames) {
63-
trigger.addEventListener(name, entry.listener, eventListenerOptions);
64-
}
64+
// Ensure that the handler runs in the NgZone since it gets
65+
// registered in `afterRender` which runs outside.
66+
injector.get(NgZone).run(() => {
67+
for (const name of interactionEventNames) {
68+
trigger.addEventListener(name, entry!.listener, eventListenerOptions);
69+
}
70+
});
6571
}
6672

6773
entry.callbacks.add(callback);
@@ -84,15 +90,21 @@ export function onInteraction(trigger: Element, callback: VoidFunction) {
8490
* Registers a hover trigger.
8591
* @param trigger Element that is the trigger.
8692
* @param callback Callback to be invoked when the trigger is hovered over.
93+
* @param injector Injector that can be used by the trigger to resolve DI tokens.
8794
*/
88-
export function onHover(trigger: Element, callback: VoidFunction): VoidFunction {
95+
export function onHover(
96+
trigger: Element, callback: VoidFunction, injector: Injector): VoidFunction {
8997
let entry = hoverTriggers.get(trigger);
9098

9199
// If this is the first entry for this element, add the listener.
92100
if (!entry) {
93101
entry = new DeferEventEntry();
94-
trigger.addEventListener('mouseenter', entry.listener, eventListenerOptions);
95102
hoverTriggers.set(trigger, entry);
103+
// Ensure that the handler runs in the NgZone since it gets
104+
// registered in `afterRender` which runs outside.
105+
injector.get(NgZone).run(() => {
106+
trigger.addEventListener('mouseenter', entry!.listener, eventListenerOptions);
107+
});
96108
}
97109

98110
entry.callbacks.add(callback);

packages/core/test/acceptance/defer_spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,7 @@ describe('@defer', () => {
12251225
template: 'Primary block content.',
12261226
})
12271227
class NestedCmp {
1228+
@Input() block!: string;
12281229
}
12291230

12301231
@Component({
@@ -1401,6 +1402,7 @@ describe('@defer', () => {
14011402
template: 'Primary block content.',
14021403
})
14031404
class NestedCmp {
1405+
@Input() block!: string;
14041406
}
14051407

14061408
@Component({
@@ -1954,6 +1956,32 @@ describe('@defer', () => {
19541956
expect(spy).toHaveBeenCalledWith('keydown', jasmine.any(Function), jasmine.any(Object));
19551957
}));
19561958

1959+
it('should bind the trigger events inside the NgZone', fakeAsync(() => {
1960+
@Component({
1961+
standalone: true,
1962+
template: `
1963+
@defer (on interaction(trigger)) {
1964+
Main content
1965+
}
1966+
1967+
<button #trigger></button>
1968+
`
1969+
})
1970+
class MyCmp {
1971+
}
1972+
1973+
const eventsInZone: Record<string, boolean> = {};
1974+
const fixture = TestBed.createComponent(MyCmp);
1975+
const button = fixture.nativeElement.querySelector('button');
1976+
1977+
spyOn(button, 'addEventListener').and.callFake((name: string) => {
1978+
eventsInZone[name] = NgZone.isInAngularZone();
1979+
});
1980+
fixture.detectChanges();
1981+
1982+
expect(eventsInZone).toEqual({click: true, keydown: true});
1983+
}));
1984+
19571985
it('should prefetch resources on interaction', fakeAsync(() => {
19581986
@Component({
19591987
standalone: true,
@@ -2252,6 +2280,32 @@ describe('@defer', () => {
22522280
expect(spy).toHaveBeenCalledWith('mouseenter', jasmine.any(Function), jasmine.any(Object));
22532281
}));
22542282

2283+
it('should bind the trigger events inside the NgZone', fakeAsync(() => {
2284+
@Component({
2285+
standalone: true,
2286+
template: `
2287+
@defer (on hover(trigger)) {
2288+
Main content
2289+
}
2290+
2291+
<button #trigger></button>
2292+
`
2293+
})
2294+
class MyCmp {
2295+
}
2296+
2297+
const eventsInZone: Record<string, boolean> = {};
2298+
const fixture = TestBed.createComponent(MyCmp);
2299+
const button = fixture.nativeElement.querySelector('button');
2300+
2301+
spyOn(button, 'addEventListener').and.callFake((name: string) => {
2302+
eventsInZone[name] = NgZone.isInAngularZone();
2303+
});
2304+
fixture.detectChanges();
2305+
2306+
expect(eventsInZone).toEqual({mouseenter: true});
2307+
}));
2308+
22552309
it('should prefetch resources on hover', fakeAsync(() => {
22562310
// Domino doesn't support creating custom events so we have to skip this test.
22572311
if (!isBrowser) {

0 commit comments

Comments
 (0)