Skip to content

Commit 08f4acf

Browse files
authored
perf(cdk/overlay): add tree-shakeable alternatives for overlay APIs (#30904)
Currently all the overlay APIs go through the `Overlay` service which means that even if an app only uses the fairly simple global positioning strategy, they'd still bring in all the code for the complex flexible positioning strategy. These changes break up the APIs into constructor functions that can be tree shaken separately. Note that I'll send follow-up PRs to roll this out in Material in order to see the full benefits. From a simple test of a an `ng new` app that only uses `MatTooltip`, this shaved off ~10kb of minified JS.
1 parent cf61960 commit 08f4acf

13 files changed

+180
-126
lines changed

goldens/cdk/overlay/index.api.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,27 @@ export class ConnectionPositionPair {
197197
panelClass?: string | string[] | undefined;
198198
}
199199

200+
// @public
201+
export function createBlockScrollStrategy(injector: Injector): BlockScrollStrategy;
202+
203+
// @public
204+
export function createCloseScrollStrategy(injector: Injector, config?: CloseScrollStrategyConfig): CloseScrollStrategy;
205+
206+
// @public
207+
export function createFlexibleConnectedPositionStrategy(injector: Injector, origin: FlexibleConnectedPositionStrategyOrigin): FlexibleConnectedPositionStrategy;
208+
209+
// @public
210+
export function createGlobalPositionStrategy(_injector: Injector): GlobalPositionStrategy;
211+
212+
// @public
213+
export function createNoopScrollStrategy(): NoopScrollStrategy;
214+
215+
// @public
216+
export function createOverlayRef(injector: Injector, config?: OverlayConfig): OverlayRef;
217+
218+
// @public
219+
export function createRepositionScrollStrategy(injector: Injector, config?: RepositionScrollStrategyConfig): RepositionScrollStrategy;
220+
200221
// @public
201222
export class FlexibleConnectedPositionStrategy implements PositionStrategy {
202223
constructor(connectedTo: FlexibleConnectedPositionStrategyOrigin, _viewportRuler: ViewportRuler, _document: Document, _platform: Platform, _overlayContainer: OverlayContainer);

src/cdk/overlay/fullscreen-overlay-container.spec.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('FullscreenOverlayContainer', () => {
2323
// stubs here, we should reconsider whether to use a Proxy instead. Avoiding a proxy for
2424
// now since it isn't supported on IE. See:
2525
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
26-
fakeDocument = {
26+
return {
2727
body: document.body,
2828
head: document.head,
2929
fullscreenElement: document.createElement('div'),
@@ -51,20 +51,15 @@ describe('FullscreenOverlayContainer', () => {
5151
createTextNode: (...args: [string]) => document.createTextNode(...args),
5252
createComment: (...args: [string]) => document.createComment(...args),
5353
};
54-
55-
return fakeDocument;
5654
},
5755
},
5856
],
5957
});
6058

6159
overlay = TestBed.inject(Overlay);
60+
fakeDocument = TestBed.inject(DOCUMENT);
6261
}));
6362

64-
afterEach(() => {
65-
fakeDocument = null;
66-
});
67-
6863
it('should open an overlay inside a fullscreen element and move it to the body', () => {
6964
const fixture = TestBed.createComponent(TestComponentWithTemplatePortals);
7065
fixture.detectChanges();

src/cdk/overlay/overlay.ts

Lines changed: 52 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
inject,
2020
RendererFactory2,
2121
DOCUMENT,
22+
Renderer2,
2223
} from '@angular/core';
2324
import {_IdGenerator} from '../a11y';
2425
import {_CdkPrivateStyleLoader} from '../private';
@@ -30,6 +31,56 @@ import {OverlayRef} from './overlay-ref';
3031
import {OverlayPositionBuilder} from './position/overlay-position-builder';
3132
import {ScrollStrategyOptions} from './scroll/index';
3233

34+
/**
35+
* Creates an overlay.
36+
* @param injector Injector to use when resolving the overlay's dependencies.
37+
* @param config Configuration applied to the overlay.
38+
* @returns Reference to the created overlay.
39+
*/
40+
export function createOverlayRef(injector: Injector, config?: OverlayConfig): OverlayRef {
41+
// This is done in the overlay container as well, but we have it here
42+
// since it's common to mock out the overlay container in tests.
43+
injector.get(_CdkPrivateStyleLoader).load(_CdkOverlayStyleLoader);
44+
45+
const overlayContainer = injector.get(OverlayContainer);
46+
const doc = injector.get(DOCUMENT);
47+
const idGenerator = injector.get(_IdGenerator);
48+
const appRef = injector.get(ApplicationRef);
49+
const directionality = injector.get(Directionality);
50+
51+
const host = doc.createElement('div');
52+
const pane = doc.createElement('div');
53+
54+
pane.id = idGenerator.getId('cdk-overlay-');
55+
pane.classList.add('cdk-overlay-pane');
56+
host.appendChild(pane);
57+
overlayContainer.getContainerElement().appendChild(host);
58+
59+
const portalOutlet = new DomPortalOutlet(pane, appRef, injector);
60+
const overlayConfig = new OverlayConfig(config);
61+
const renderer =
62+
injector.get(Renderer2, null, {optional: true}) ||
63+
injector.get(RendererFactory2).createRenderer(null, null);
64+
65+
overlayConfig.direction = overlayConfig.direction || directionality.value;
66+
67+
return new OverlayRef(
68+
portalOutlet,
69+
host,
70+
pane,
71+
overlayConfig,
72+
injector.get(NgZone),
73+
injector.get(OverlayKeyboardDispatcher),
74+
doc,
75+
injector.get(Location),
76+
injector.get(OverlayOutsideClickDispatcher),
77+
config?.disableAnimations ??
78+
injector.get(ANIMATION_MODULE_TYPE, null, {optional: true}) === 'NoopAnimations',
79+
injector.get(EnvironmentInjector),
80+
renderer,
81+
);
82+
}
83+
3384
/**
3485
* Service to create Overlays. Overlays are dynamically added pieces of floating UI, meant to be
3586
* used as a low-level building block for other components. Dialogs, tooltips, menus,
@@ -41,21 +92,8 @@ import {ScrollStrategyOptions} from './scroll/index';
4192
@Injectable({providedIn: 'root'})
4293
export class Overlay {
4394
scrollStrategies = inject(ScrollStrategyOptions);
44-
private _overlayContainer = inject(OverlayContainer);
4595
private _positionBuilder = inject(OverlayPositionBuilder);
46-
private _keyboardDispatcher = inject(OverlayKeyboardDispatcher);
4796
private _injector = inject(Injector);
48-
private _ngZone = inject(NgZone);
49-
private _document = inject(DOCUMENT);
50-
private _directionality = inject(Directionality);
51-
private _location = inject(Location);
52-
private _outsideClickDispatcher = inject(OverlayOutsideClickDispatcher);
53-
private _animationsModuleType = inject(ANIMATION_MODULE_TYPE, {optional: true});
54-
private _idGenerator = inject(_IdGenerator);
55-
private _renderer = inject(RendererFactory2).createRenderer(null, null);
56-
57-
private _appRef: ApplicationRef;
58-
private _styleLoader = inject(_CdkPrivateStyleLoader);
5997

6098
constructor(...args: unknown[]);
6199
constructor() {}
@@ -66,31 +104,7 @@ export class Overlay {
66104
* @returns Reference to the created overlay.
67105
*/
68106
create(config?: OverlayConfig): OverlayRef {
69-
// This is done in the overlay container as well, but we have it here
70-
// since it's common to mock out the overlay container in tests.
71-
this._styleLoader.load(_CdkOverlayStyleLoader);
72-
73-
const host = this._createHostElement();
74-
const pane = this._createPaneElement(host);
75-
const portalOutlet = this._createPortalOutlet(pane);
76-
const overlayConfig = new OverlayConfig(config);
77-
78-
overlayConfig.direction = overlayConfig.direction || this._directionality.value;
79-
80-
return new OverlayRef(
81-
portalOutlet,
82-
host,
83-
pane,
84-
overlayConfig,
85-
this._ngZone,
86-
this._keyboardDispatcher,
87-
this._document,
88-
this._location,
89-
this._outsideClickDispatcher,
90-
config?.disableAnimations ?? this._animationsModuleType === 'NoopAnimations',
91-
this._injector.get(EnvironmentInjector),
92-
this._renderer,
93-
);
107+
return createOverlayRef(this._injector, config);
94108
}
95109

96110
/**
@@ -101,44 +115,4 @@ export class Overlay {
101115
position(): OverlayPositionBuilder {
102116
return this._positionBuilder;
103117
}
104-
105-
/**
106-
* Creates the DOM element for an overlay and appends it to the overlay container.
107-
* @returns Newly-created pane element
108-
*/
109-
private _createPaneElement(host: HTMLElement): HTMLElement {
110-
const pane = this._document.createElement('div');
111-
112-
pane.id = this._idGenerator.getId('cdk-overlay-');
113-
pane.classList.add('cdk-overlay-pane');
114-
host.appendChild(pane);
115-
116-
return pane;
117-
}
118-
119-
/**
120-
* Creates the host element that wraps around an overlay
121-
* and can be used for advanced positioning.
122-
* @returns Newly-create host element.
123-
*/
124-
private _createHostElement(): HTMLElement {
125-
const host = this._document.createElement('div');
126-
this._overlayContainer.getContainerElement().appendChild(host);
127-
return host;
128-
}
129-
130-
/**
131-
* Create a DomPortalOutlet into which the overlay content can be loaded.
132-
* @param pane The DOM element to turn into a portal outlet.
133-
* @returns A portal outlet for the given DOM element.
134-
*/
135-
private _createPortalOutlet(pane: HTMLElement): DomPortalOutlet {
136-
// We have to resolve the ApplicationRef later in order to allow people
137-
// to use overlay-based providers during app initialization.
138-
if (!this._appRef) {
139-
this._appRef = this._injector.get<ApplicationRef>(ApplicationRef);
140-
}
141-
142-
return new DomPortalOutlet(pane, this._appRef, this._injector);
143-
}
144118
}

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

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

99
import {PositionStrategy} from './position-strategy';
10-
import {ElementRef} from '@angular/core';
10+
import {DOCUMENT, ElementRef, Injector} from '@angular/core';
1111
import {ViewportRuler, CdkScrollable, ViewportScrollPosition} from '../../scrolling';
1212
import {
1313
ConnectedOverlayPositionChange,
@@ -44,6 +44,24 @@ export type FlexibleConnectedPositionStrategyOrigin =
4444
/** Equivalent of `DOMRect` without some of the properties we don't care about. */
4545
type Dimensions = Omit<DOMRect, 'x' | 'y' | 'toJSON'>;
4646

47+
/**
48+
* Creates a flexible position strategy.
49+
* @param injector Injector used to resolve dependnecies for the position strategy.
50+
* @param origin Origin relative to which to position the overlay.
51+
*/
52+
export function createFlexibleConnectedPositionStrategy(
53+
injector: Injector,
54+
origin: FlexibleConnectedPositionStrategyOrigin,
55+
): FlexibleConnectedPositionStrategy {
56+
return new FlexibleConnectedPositionStrategy(
57+
origin,
58+
injector.get(ViewportRuler),
59+
injector.get(DOCUMENT),
60+
injector.get(Platform),
61+
injector.get(OverlayContainer),
62+
);
63+
}
64+
4765
/**
4866
* A strategy for positioning overlays. Using this strategy, an overlay is given an
4967
* implicit position relative some origin element. The relative position is defined in terms of

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,23 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {Injector} from '@angular/core';
910
import {OverlayRef} from '../overlay-ref';
1011
import {PositionStrategy} from './position-strategy';
1112

1213
/** Class to be added to the overlay pane wrapper. */
1314
const wrapperClass = 'cdk-global-overlay-wrapper';
1415

16+
/**
17+
* Creates a global position strategy.
18+
* @param injector Injector used to resolve dependencies for the strategy.
19+
*/
20+
export function createGlobalPositionStrategy(_injector: Injector): GlobalPositionStrategy {
21+
// Note: `injector` is unused, but we may need it in
22+
// the future which would introduce a breaking change.
23+
return new GlobalPositionStrategy();
24+
}
25+
1526
/**
1627
* A strategy for positioning overlays. Using this strategy, an overlay is given an
1728
* explicit position relative to the browser's viewport. We use flexbox, instead of

src/cdk/overlay/position/overlay-position-builder.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,18 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {Platform} from '../../platform';
10-
import {ViewportRuler} from '../../scrolling';
11-
12-
import {Injectable, inject, DOCUMENT} from '@angular/core';
13-
import {OverlayContainer} from '../overlay-container';
9+
import {Injectable, Injector, inject} from '@angular/core';
1410
import {
11+
createFlexibleConnectedPositionStrategy,
1512
FlexibleConnectedPositionStrategy,
1613
FlexibleConnectedPositionStrategyOrigin,
1714
} from './flexible-connected-position-strategy';
18-
import {GlobalPositionStrategy} from './global-position-strategy';
15+
import {createGlobalPositionStrategy, GlobalPositionStrategy} from './global-position-strategy';
1916

2017
/** Builder for overlay position strategy. */
2118
@Injectable({providedIn: 'root'})
2219
export class OverlayPositionBuilder {
23-
private _viewportRuler = inject(ViewportRuler);
24-
private _document = inject(DOCUMENT);
25-
private _platform = inject(Platform);
26-
private _overlayContainer = inject(OverlayContainer);
20+
private _injector = inject(Injector);
2721

2822
constructor(...args: unknown[]);
2923
constructor() {}
@@ -32,7 +26,7 @@ export class OverlayPositionBuilder {
3226
* Creates a global position strategy.
3327
*/
3428
global(): GlobalPositionStrategy {
35-
return new GlobalPositionStrategy();
29+
return createGlobalPositionStrategy(this._injector);
3630
}
3731

3832
/**
@@ -42,12 +36,6 @@ export class OverlayPositionBuilder {
4236
flexibleConnectedTo(
4337
origin: FlexibleConnectedPositionStrategyOrigin,
4438
): FlexibleConnectedPositionStrategy {
45-
return new FlexibleConnectedPositionStrategy(
46-
origin,
47-
this._viewportRuler,
48-
this._document,
49-
this._platform,
50-
this._overlayContainer,
51-
);
39+
return createFlexibleConnectedPositionStrategy(this._injector, origin);
5240
}
5341
}

src/cdk/overlay/public-api.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export * from './position/connected-position';
1111
export * from './scroll/index';
1212
export * from './overlay-module';
1313
export * from './dispatchers/index';
14-
export {Overlay} from './overlay';
14+
export {Overlay, createOverlayRef} from './overlay';
1515
export {OverlayContainer} from './overlay-container';
1616
export {CdkOverlayOrigin, CdkConnectedOverlay} from './overlay-directives';
1717
export {FullscreenOverlayContainer} from './fullscreen-overlay-container';
@@ -22,11 +22,15 @@ export {OverlayPositionBuilder} from './position/overlay-position-builder';
2222

2323
// Export pre-defined position strategies and interface to build custom ones.
2424
export {PositionStrategy} from './position/position-strategy';
25-
export {GlobalPositionStrategy} from './position/global-position-strategy';
25+
export {
26+
GlobalPositionStrategy,
27+
createGlobalPositionStrategy,
28+
} from './position/global-position-strategy';
2629
export {
2730
ConnectedPosition,
2831
FlexibleConnectedPositionStrategy,
2932
FlexibleConnectedPositionStrategyOrigin,
3033
STANDARD_DROPDOWN_ADJACENT_POSITIONS,
3134
STANDARD_DROPDOWN_BELOW_POSITIONS,
35+
createFlexibleConnectedPositionStrategy,
3236
} from './position/flexible-connected-position-strategy';

src/cdk/overlay/scroll/block-scroll-strategy.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,23 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {DOCUMENT, Injector} from '@angular/core';
910
import {ScrollStrategy} from './scroll-strategy';
1011
import {ViewportRuler} from '../../scrolling';
1112
import {coerceCssPixelValue} from '../../coercion';
1213
import {supportsScrollBehavior} from '../../platform';
1314

1415
const scrollBehaviorSupported = supportsScrollBehavior();
1516

17+
/**
18+
* Creates a scroll strategy that prevents the user from scrolling while the overlay is open.
19+
* @param injector Injector used to resolve dependencies of the scroll strategy.
20+
* @param config Configuration options for the scroll strategy.
21+
*/
22+
export function createBlockScrollStrategy(injector: Injector): BlockScrollStrategy {
23+
return new BlockScrollStrategy(injector.get(ViewportRuler), injector.get(DOCUMENT));
24+
}
25+
1626
/**
1727
* Strategy that will prevent the user from scrolling while the overlay is visible.
1828
*/

0 commit comments

Comments
 (0)