Skip to content

Commit 7ec1e48

Browse files
atscottmmalerba
andauthored
refactor(cdk/overlay): Reduce dependency on NgZone (#28332)
* fix(cdk/overlay): Allow cdk overlay to work with `NoopNgZone` This updates the overlay implementation to use `afterNextRender` instead of `ngZone.onStable` which does not emit at all in zoneless applications. For zone-based applications, this means that the overlay will be positioned immediately after the next ApplicationRef tick. This is slightly different from `onStable`, which with ZoneJS can span multiple change detection rounds. * test: update API golden --------- Co-authored-by: Miles Malerba <[email protected]>
1 parent 1012304 commit 7ec1e48

File tree

6 files changed

+41
-55
lines changed

6 files changed

+41
-55
lines changed

src/cdk/overlay/overlay-ref.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@
88

99
import {Direction, Directionality} from '@angular/cdk/bidi';
1010
import {ComponentPortal, Portal, PortalOutlet, TemplatePortal} from '@angular/cdk/portal';
11-
import {ComponentRef, EmbeddedViewRef, NgZone} from '@angular/core';
11+
import {
12+
ComponentRef,
13+
EmbeddedViewRef,
14+
EnvironmentInjector,
15+
NgZone,
16+
afterNextRender,
17+
} from '@angular/core';
1218
import {Location} from '@angular/common';
1319
import {Observable, Subject, merge, SubscriptionLike, Subscription} from 'rxjs';
14-
import {take, takeUntil} from 'rxjs/operators';
20+
import {takeUntil} from 'rxjs/operators';
1521
import {OverlayKeyboardDispatcher} from './dispatchers/overlay-keyboard-dispatcher';
1622
import {OverlayOutsideClickDispatcher} from './dispatchers/overlay-outside-click-dispatcher';
1723
import {OverlayConfig} from './overlay-config';
@@ -65,6 +71,7 @@ export class OverlayRef implements PortalOutlet {
6571
private _location: Location,
6672
private _outsideClickDispatcher: OverlayOutsideClickDispatcher,
6773
private _animationsDisabled = false,
74+
private _injector: EnvironmentInjector,
6875
) {
6976
if (_config.scrollStrategy) {
7077
this._scrollStrategy = _config.scrollStrategy;
@@ -125,15 +132,17 @@ export class OverlayRef implements PortalOutlet {
125132
this._scrollStrategy.enable();
126133
}
127134

128-
// Update the position once the zone is stable so that the overlay will be fully rendered
129-
// before attempting to position it, as the position may depend on the size of the rendered
130-
// content.
131-
this._ngZone.onStable.pipe(take(1)).subscribe(() => {
132-
// The overlay could've been detached before the zone has stabilized.
133-
if (this.hasAttached()) {
134-
this.updatePosition();
135-
}
136-
});
135+
// Update the position once the overlay is fully rendered before attempting to position it,
136+
// as the position may depend on the size of the rendered content.
137+
afterNextRender(
138+
() => {
139+
// The overlay could've been detached before the callback executed.
140+
if (this.hasAttached()) {
141+
this.updatePosition();
142+
}
143+
},
144+
{injector: this._injector},
145+
);
137146

138147
// Enable pointer events for the overlay pane element.
139148
this._togglePointerEvents(true);

src/cdk/overlay/overlay.spec.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
Type,
1818
} from '@angular/core';
1919
import {Direction, Directionality} from '@angular/cdk/bidi';
20-
import {MockNgZone, dispatchFakeEvent} from '../testing/private';
20+
import {dispatchFakeEvent} from '../testing/private';
2121
import {ComponentPortal, TemplatePortal, CdkPortal} from '@angular/cdk/portal';
2222
import {Location} from '@angular/common';
2323
import {SpyLocation} from '@angular/common/testing';
@@ -40,7 +40,6 @@ describe('Overlay', () => {
4040
let overlayContainer: OverlayContainer;
4141
let viewContainerFixture: ComponentFixture<TestComponentWithTemplatePortals>;
4242
let dir: Direction;
43-
let zone: MockNgZone;
4443
let mockLocation: SpyLocation;
4544

4645
function setup(imports: Type<unknown>[] = []) {
@@ -56,10 +55,6 @@ describe('Overlay', () => {
5655
return fakeDirectionality;
5756
},
5857
},
59-
{
60-
provide: NgZone,
61-
useFactory: () => (zone = new MockNgZone()),
62-
},
6358
{
6459
provide: Location,
6560
useClass: SpyLocation,
@@ -404,7 +399,6 @@ describe('Overlay', () => {
404399
.toBeTruthy();
405400

406401
viewContainerFixture.detectChanges();
407-
zone.simulateZoneExit();
408402

409403
expect(overlayRef.hostElement.parentElement)
410404
.withContext('Expected host element to have been removed once the zone stabilizes.')
@@ -510,7 +504,6 @@ describe('Overlay', () => {
510504

511505
overlay.create(config).attach(componentPortal);
512506
viewContainerFixture.detectChanges();
513-
zone.simulateZoneExit();
514507
tick();
515508

516509
expect(overlayContainerElement.querySelectorAll('.fake-positioned').length).toBe(1);
@@ -533,7 +526,6 @@ describe('Overlay', () => {
533526
.toBeTruthy();
534527

535528
overlayRef.detach();
536-
zone.simulateZoneExit();
537529
tick();
538530

539531
overlayRef.attach(componentPortal);
@@ -573,7 +565,6 @@ describe('Overlay', () => {
573565
const overlayRef = overlay.create(config);
574566
overlayRef.attach(componentPortal);
575567
viewContainerFixture.detectChanges();
576-
zone.simulateZoneExit();
577568
tick();
578569

579570
expect(firstStrategy.attach).toHaveBeenCalledTimes(1);
@@ -606,7 +597,6 @@ describe('Overlay', () => {
606597
const overlayRef = overlay.create(config);
607598
overlayRef.attach(componentPortal);
608599
viewContainerFixture.detectChanges();
609-
zone.simulateZoneExit();
610600
tick();
611601

612602
expect(strategy.attach).toHaveBeenCalledTimes(1);
@@ -889,7 +879,6 @@ describe('Overlay', () => {
889879

890880
overlayRef.detach();
891881
dispatchFakeEvent(backdrop, 'transitionend');
892-
zone.simulateZoneExit();
893882
viewContainerFixture.detectChanges();
894883

895884
backdrop.click();
@@ -947,7 +936,6 @@ describe('Overlay', () => {
947936
.toContain('custom-panel-class');
948937

949938
overlayRef.detach();
950-
zone.simulateZoneExit();
951939
viewContainerFixture.detectChanges();
952940
expect(pane.classList).not.toContain('custom-panel-class', 'Expected class to be removed');
953941

@@ -971,13 +959,13 @@ describe('Overlay', () => {
971959
.toContain('custom-panel-class');
972960

973961
overlayRef.detach();
974-
viewContainerFixture.detectChanges();
975-
976-
expect(pane.classList)
977-
.withContext('Expected class not to be removed immediately')
978-
.toContain('custom-panel-class');
979-
980-
zone.simulateZoneExit();
962+
// Stable emits after zone.run
963+
TestBed.inject(NgZone).run(() => {
964+
viewContainerFixture.detectChanges();
965+
expect(pane.classList)
966+
.withContext('Expected class not to be removed immediately')
967+
.toContain('custom-panel-class');
968+
});
981969

982970
expect(pane.classList)
983971
.not.withContext('Expected class to be removed on stable')
@@ -1061,7 +1049,6 @@ describe('Overlay', () => {
10611049

10621050
overlayRef.attach(componentPortal);
10631051
viewContainerFixture.detectChanges();
1064-
zone.simulateZoneExit();
10651052
tick();
10661053

10671054
expect(firstStrategy.attach).toHaveBeenCalledTimes(1);
@@ -1095,7 +1082,6 @@ describe('Overlay', () => {
10951082

10961083
overlayRef.attach(componentPortal);
10971084
viewContainerFixture.detectChanges();
1098-
zone.simulateZoneExit();
10991085
tick();
11001086

11011087
expect(strategy.attach).toHaveBeenCalledTimes(1);

src/cdk/overlay/overlay.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
NgZone,
1919
ANIMATION_MODULE_TYPE,
2020
Optional,
21+
EnvironmentInjector,
2122
} from '@angular/core';
2223
import {OverlayKeyboardDispatcher} from './dispatchers/overlay-keyboard-dispatcher';
2324
import {OverlayOutsideClickDispatcher} from './dispatchers/overlay-outside-click-dispatcher';
@@ -85,6 +86,7 @@ export class Overlay {
8586
this._location,
8687
this._outsideClickDispatcher,
8788
this._animationsModuleType === 'NoopAnimations',
89+
this._injector.get(EnvironmentInjector),
8890
);
8991
}
9092

src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {ComponentPortal, PortalModule} from '@angular/cdk/portal';
22
import {CdkScrollable, ScrollingModule, ViewportRuler} from '@angular/cdk/scrolling';
3-
import {dispatchFakeEvent, MockNgZone} from '../../testing/private';
4-
import {Component, ElementRef, NgZone} from '@angular/core';
5-
import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
3+
import {dispatchFakeEvent} from '../../testing/private';
4+
import {ApplicationRef, Component, ElementRef} from '@angular/core';
5+
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
66
import {Subscription} from 'rxjs';
77
import {map} from 'rxjs/operators';
88
import {
@@ -22,24 +22,17 @@ const DEFAULT_WIDTH = 60;
2222
describe('FlexibleConnectedPositionStrategy', () => {
2323
let overlay: Overlay;
2424
let overlayContainer: OverlayContainer;
25-
let zone: MockNgZone;
2625
let overlayRef: OverlayRef;
2726
let viewport: ViewportRuler;
2827

2928
beforeEach(() => {
3029
TestBed.configureTestingModule({
3130
imports: [ScrollingModule, OverlayModule, PortalModule, TestOverlay],
32-
providers: [{provide: NgZone, useFactory: () => (zone = new MockNgZone())}],
3331
});
3432

35-
inject(
36-
[Overlay, OverlayContainer, ViewportRuler],
37-
(o: Overlay, oc: OverlayContainer, v: ViewportRuler) => {
38-
overlay = o;
39-
overlayContainer = oc;
40-
viewport = v;
41-
},
42-
)();
33+
overlay = TestBed.inject(Overlay);
34+
overlayContainer = TestBed.inject(OverlayContainer);
35+
viewport = TestBed.inject(ViewportRuler);
4336
});
4437

4538
afterEach(() => {
@@ -53,7 +46,7 @@ describe('FlexibleConnectedPositionStrategy', () => {
5346
function attachOverlay(config: OverlayConfig) {
5447
overlayRef = overlay.create(config);
5548
overlayRef.attach(new ComponentPortal(TestOverlay));
56-
zone.simulateZoneExit();
49+
TestBed.inject(ApplicationRef).tick();
5750
}
5851

5952
it('should throw when attempting to attach to multiple different overlays', () => {
@@ -1499,7 +1492,6 @@ describe('FlexibleConnectedPositionStrategy', () => {
14991492

15001493
window.scroll(0, 100);
15011494
overlayRef.updatePosition();
1502-
zone.simulateZoneExit();
15031495

15041496
overlayRect = overlayRef.overlayElement.getBoundingClientRect();
15051497
expect(Math.floor(overlayRect.top))
@@ -1547,7 +1539,6 @@ describe('FlexibleConnectedPositionStrategy', () => {
15471539

15481540
window.scroll(0, scrollBy);
15491541
overlayRef.updatePosition();
1550-
zone.simulateZoneExit();
15511542

15521543
let currentOverlayTop = Math.floor(overlayRef.overlayElement.getBoundingClientRect().top);
15531544

src/cdk/overlay/position/global-position-strategy.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
import {NgZone, Component} from '@angular/core';
1+
import {Component, ApplicationRef} from '@angular/core';
22
import {TestBed} from '@angular/core/testing';
3-
import {MockNgZone} from '../../testing/private';
43
import {PortalModule, ComponentPortal} from '@angular/cdk/portal';
54
import {OverlayModule, Overlay, OverlayConfig, OverlayRef} from '../index';
65

76
describe('GlobalPositonStrategy', () => {
87
let overlayRef: OverlayRef;
98
let overlay: Overlay;
10-
let zone: MockNgZone;
119

1210
beforeEach(() => {
1311
TestBed.configureTestingModule({
1412
imports: [OverlayModule, PortalModule, BlankPortal],
15-
providers: [{provide: NgZone, useFactory: () => (zone = new MockNgZone())}],
1613
});
1714

1815
overlay = TestBed.inject(Overlay);
@@ -29,7 +26,7 @@ describe('GlobalPositonStrategy', () => {
2926
const portal = new ComponentPortal(BlankPortal);
3027
overlayRef = overlay.create(config);
3128
overlayRef.attach(portal);
32-
zone.simulateZoneExit();
29+
TestBed.inject(ApplicationRef).tick();
3330
return overlayRef;
3431
}
3532

tools/public_api_guard/cdk/overlay.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Direction } from '@angular/cdk/bidi';
1313
import { Directionality } from '@angular/cdk/bidi';
1414
import { ElementRef } from '@angular/core';
1515
import { EmbeddedViewRef } from '@angular/core';
16+
import { EnvironmentInjector } from '@angular/core';
1617
import { EventEmitter } from '@angular/core';
1718
import * as i0 from '@angular/core';
1819
import * as i1 from '@angular/cdk/bidi';
@@ -359,7 +360,7 @@ export class OverlayPositionBuilder {
359360

360361
// @public
361362
export class OverlayRef implements PortalOutlet {
362-
constructor(_portalOutlet: PortalOutlet, _host: HTMLElement, _pane: HTMLElement, _config: ImmutableObject<OverlayConfig>, _ngZone: NgZone, _keyboardDispatcher: OverlayKeyboardDispatcher, _document: Document, _location: Location_2, _outsideClickDispatcher: OverlayOutsideClickDispatcher, _animationsDisabled?: boolean);
363+
constructor(_portalOutlet: PortalOutlet, _host: HTMLElement, _pane: HTMLElement, _config: ImmutableObject<OverlayConfig>, _ngZone: NgZone, _keyboardDispatcher: OverlayKeyboardDispatcher, _document: Document, _location: Location_2, _outsideClickDispatcher: OverlayOutsideClickDispatcher, _animationsDisabled: boolean, _injector: EnvironmentInjector);
363364
addPanelClass(classes: string | string[]): void;
364365
// (undocumented)
365366
attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;

0 commit comments

Comments
 (0)