Skip to content

Commit ea6230e

Browse files
committed
perf(cdk/overlay): add tree-shakeable alternatives for overlay APIs
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 204b289 commit ea6230e

13 files changed

+180
-126
lines changed

Diff for: goldens/cdk/overlay/index.api.md

+21
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);

Diff for: src/cdk/overlay/fullscreen-overlay-container.spec.ts

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

6260
overlay = TestBed.inject(Overlay);
61+
fakeDocument = TestBed.inject(DOCUMENT);
6362
}));
6463

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

Diff for: src/cdk/overlay/overlay.ts

+52-78
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
EnvironmentInjector,
1919
inject,
2020
RendererFactory2,
21+
Renderer2,
2122
} from '@angular/core';
2223
import {_IdGenerator} from '../a11y';
2324
import {_CdkPrivateStyleLoader} from '../private';
@@ -29,6 +30,56 @@ import {OverlayRef} from './overlay-ref';
2930
import {OverlayPositionBuilder} from './position/overlay-position-builder';
3031
import {ScrollStrategyOptions} from './scroll/index';
3132

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

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

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

Diff for: src/cdk/overlay/position/flexible-connected-position-strategy.ts

+19-1
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

Diff for: src/cdk/overlay/position/global-position-strategy.ts

+11
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

Diff for: src/cdk/overlay/position/overlay-position-builder.ts

+6-18
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-
import {DOCUMENT} from '@angular/common';
12-
import {Injectable, inject} 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
}

Diff for: src/cdk/overlay/public-api.ts

+6-2
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';

Diff for: src/cdk/overlay/scroll/block-scroll-strategy.ts

+10
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)