Skip to content

Commit 82d632f

Browse files
committed
feat(overlay): more flexible scroll strategy API and ability to define/override custom strategies
* Refactors the overlay setup to allow for scroll strategies to be passed in by name, instead of by instance. * Handles the scroll strategy dependency injection automatically. * Adds an API for registering custom scroll strategies and overriding the existing ones. * Adds a second parameter to the `attach` method, allowing for a config object to be passed in. * Throws an error if there's an attempt to attach a scroll strategy multiple times. This is mostly a sanity check to ensure that we don't cache the scroll strategy instances. Relates to #4093.
1 parent 3569805 commit 82d632f

19 files changed

+210
-86
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {MdOptionSelectionChange, MdOption} from '../core/option/option';
2222
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
2323
import {Dir} from '../core/rtl/dir';
2424
import {MdInputContainer} from '../input/input-container';
25-
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
2625
import {Subscription} from 'rxjs/Subscription';
2726
import 'rxjs/add/observable/merge';
2827
import 'rxjs/add/observable/fromEvent';
@@ -104,7 +103,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
104103
constructor(private _element: ElementRef, private _overlay: Overlay,
105104
private _viewContainerRef: ViewContainerRef,
106105
private _changeDetectorRef: ChangeDetectorRef,
107-
private _scrollDispatcher: ScrollDispatcher,
108106
@Optional() private _dir: Dir, private _zone: NgZone,
109107
@Optional() @Host() private _inputContainer: MdInputContainer,
110108
@Optional() @Inject(DOCUMENT) private _document: any) {}
@@ -366,7 +364,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
366364
overlayState.positionStrategy = this._getOverlayPosition();
367365
overlayState.width = this._getHostWidth();
368366
overlayState.direction = this._dir ? this._dir.value : 'ltr';
369-
overlayState.scrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher);
367+
overlayState.scrollStrategy = 'reposition';
370368
return overlayState;
371369
}
372370

src/lib/core/overlay/overlay-directives.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import {Overlay, OVERLAY_PROVIDERS} from './overlay';
1717
import {OverlayRef} from './overlay-ref';
1818
import {TemplatePortal} from '../portal/portal';
19-
import {OverlayState} from './overlay-state';
19+
import {OverlayState, OverlayStateScrollStrategy} from './overlay-state';
2020
import {
2121
ConnectionPositionPair,
2222
ConnectedOverlayPositionChange
@@ -29,7 +29,6 @@ import {RepositionScrollStrategy} from './scroll/reposition-scroll-strategy';
2929
import {ScrollStrategy} from './scroll/scroll-strategy';
3030
import {coerceBooleanProperty} from '../coercion/boolean-property';
3131
import {ESCAPE} from '../keyboard/keycodes';
32-
import {ScrollDispatcher} from './scroll/scroll-dispatcher';
3332
import {Subscription} from 'rxjs/Subscription';
3433
import {ScrollDispatchModule} from './scroll/index';
3534

@@ -125,7 +124,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
125124
@Input() backdropClass: string;
126125

127126
/** Strategy to be used when handling scroll events while the overlay is open. */
128-
@Input() scrollStrategy: ScrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher);
127+
@Input() scrollStrategy: OverlayStateScrollStrategy = 'reposition';
129128

130129
/** Whether the overlay is open. */
131130
@Input() open: boolean = false;
@@ -157,7 +156,6 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
157156
constructor(
158157
private _overlay: Overlay,
159158
private _renderer: Renderer2,
160-
private _scrollDispatcher: ScrollDispatcher,
161159
templateRef: TemplateRef<any>,
162160
viewContainerRef: ViewContainerRef,
163161
@Optional() private _dir: Dir) {

src/lib/core/overlay/overlay-ref.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ export class OverlayRef implements PortalHost {
2020
private _portalHost: PortalHost,
2121
private _pane: HTMLElement,
2222
private _state: OverlayState,
23+
private _scrollStrategy: ScrollStrategy,
2324
private _ngZone: NgZone) {
2425

25-
this._state.scrollStrategy.attach(this);
26+
_scrollStrategy.attach(this,
27+
typeof _state.scrollStrategy === 'string' ? null : _state.scrollStrategy.config);
2628
}
2729

2830
/** The overlay's HTML element */
@@ -44,7 +46,7 @@ export class OverlayRef implements PortalHost {
4446
this.updateDirection();
4547
this.updatePosition();
4648
this._attachments.next();
47-
this._state.scrollStrategy.enable();
49+
this._scrollStrategy.enable();
4850

4951
// Enable pointer events for the overlay pane element.
5052
this._togglePointerEvents(true);
@@ -67,7 +69,7 @@ export class OverlayRef implements PortalHost {
6769
// This is necessary because otherwise the pane element will cover the page and disable
6870
// pointer events therefore. Depends on the position strategy and the applied pane boundaries.
6971
this._togglePointerEvents(false);
70-
this._state.scrollStrategy.disable();
72+
this._scrollStrategy.disable();
7173
this._detachments.next();
7274

7375
return this._portalHost.detach();
@@ -81,9 +83,13 @@ export class OverlayRef implements PortalHost {
8183
this._state.positionStrategy.dispose();
8284
}
8385

86+
if (this._scrollStrategy) {
87+
this._scrollStrategy.disable();
88+
this._scrollStrategy = null;
89+
}
90+
8491
this.detachBackdrop();
8592
this._portalHost.dispose();
86-
this._state.scrollStrategy.disable();
8793
this._detachments.next();
8894
this._detachments.complete();
8995
this._attachments.complete();

src/lib/core/overlay/overlay-state.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {LayoutDirection} from '../rtl/dir';
33
import {ScrollStrategy} from './scroll/scroll-strategy';
44
import {NoopScrollStrategy} from './scroll/noop-scroll-strategy';
55

6+
export type OverlayStateScrollStrategy = string | {name: string; config: any};
67

78
/**
89
* OverlayState is a bag of values for either the initial configuration or current state of an
@@ -13,7 +14,7 @@ export class OverlayState {
1314
positionStrategy: PositionStrategy;
1415

1516
/** Strategy to be used when handling scroll events while the overlay is open. */
16-
scrollStrategy: ScrollStrategy = new NoopScrollStrategy();
17+
scrollStrategy: OverlayStateScrollStrategy = 'noop';
1718

1819
/** Whether the overlay has a backdrop. */
1920
hasBackdrop: boolean = false;

src/lib/core/overlay/overlay.spec.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ describe('Overlay', () => {
2727
return {getContainerElement: () => overlayContainerElement};
2828
}}
2929
]
30-
});
31-
32-
TestBed.compileComponents();
30+
}).compileComponents();
3331
}));
3432

3533
beforeEach(inject([Overlay], (o: Overlay) => {
@@ -340,10 +338,31 @@ describe('Overlay', () => {
340338
let fakeScrollStrategy: FakeScrollStrategy;
341339
let config: OverlayState;
342340

341+
class FakeScrollStrategy implements ScrollStrategy {
342+
isEnabled = false;
343+
overlayRef: OverlayRef;
344+
345+
constructor() {
346+
fakeScrollStrategy = this;
347+
}
348+
349+
attach(overlayRef: OverlayRef) {
350+
this.overlayRef = overlayRef;
351+
}
352+
353+
enable() {
354+
this.isEnabled = true;
355+
}
356+
357+
disable() {
358+
this.isEnabled = false;
359+
}
360+
}
361+
343362
beforeEach(() => {
344363
config = new OverlayState();
345-
fakeScrollStrategy = new FakeScrollStrategy();
346-
config.scrollStrategy = fakeScrollStrategy;
364+
overlay.registerScrollStrategy('fake', FakeScrollStrategy);
365+
config.scrollStrategy = 'fake';
347366
});
348367

349368
it('should attach the overlay ref to the scroll strategy', () => {
@@ -450,20 +469,3 @@ class FakePositionStrategy implements PositionStrategy {
450469

451470
dispose() {}
452471
}
453-
454-
class FakeScrollStrategy implements ScrollStrategy {
455-
isEnabled = false;
456-
overlayRef: OverlayRef;
457-
458-
attach(overlayRef: OverlayRef) {
459-
this.overlayRef = overlayRef;
460-
}
461-
462-
enable() {
463-
this.isEnabled = true;
464-
}
465-
466-
disable() {
467-
this.isEnabled = false;
468-
}
469-
}

src/lib/core/overlay/overlay.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ import {
55
Injector,
66
NgZone,
77
Provider,
8+
ReflectiveInjector,
89
} from '@angular/core';
910
import {OverlayState} from './overlay-state';
1011
import {DomPortalHost} from '../portal/dom-portal-host';
1112
import {OverlayRef} from './overlay-ref';
1213
import {OverlayPositionBuilder} from './position/overlay-position-builder';
1314
import {VIEWPORT_RULER_PROVIDER} from './position/viewport-ruler';
1415
import {OverlayContainer, OVERLAY_CONTAINER_PROVIDER} from './overlay-container';
16+
import {ScrollStrategy} from './scroll/scroll-strategy';
17+
import {RepositionScrollStrategy} from './scroll/reposition-scroll-strategy';
18+
import {BlockScrollStrategy} from './scroll/block-scroll-strategy';
19+
import {CloseScrollStrategy} from './scroll/close-scroll-strategy';
20+
import {NoopScrollStrategy} from './scroll/noop-scroll-strategy';
1521

1622

1723
/** Next overlay unique ID. */
@@ -31,12 +37,22 @@ let defaultState = new OverlayState();
3137
*/
3238
@Injectable()
3339
export class Overlay {
40+
// Create a child ReflectiveInjector, allowing us to instantiate scroll
41+
// strategies without going throught the injector cache.
42+
private _reflectiveInjector = ReflectiveInjector.resolveAndCreate([], this._injector);
43+
private _scrollStrategies = {
44+
reposition: RepositionScrollStrategy,
45+
block: BlockScrollStrategy,
46+
close: CloseScrollStrategy,
47+
noop: NoopScrollStrategy
48+
};
49+
3450
constructor(private _overlayContainer: OverlayContainer,
3551
private _componentFactoryResolver: ComponentFactoryResolver,
3652
private _positionBuilder: OverlayPositionBuilder,
3753
private _appRef: ApplicationRef,
3854
private _injector: Injector,
39-
private _ngZone: NgZone) {}
55+
private _ngZone: NgZone) { }
4056

4157
/**
4258
* Creates an overlay.
@@ -55,15 +71,26 @@ export class Overlay {
5571
return this._positionBuilder;
5672
}
5773

74+
/**
75+
* Registers a scroll strategy to be available for use when creating an overlay.
76+
* @param name Name of the scroll strategy.
77+
* @param constructor Class to be used to instantiate the scroll strategy.
78+
*/
79+
registerScrollStrategy(name: string, constructor: Function): void {
80+
if (name && constructor) {
81+
this._scrollStrategies[name] = constructor;
82+
}
83+
}
84+
5885
/**
5986
* Creates the DOM element for an overlay and appends it to the overlay container.
6087
* @returns Newly-created pane element
6188
*/
6289
private _createPaneElement(): HTMLElement {
6390
let pane = document.createElement('div');
91+
6492
pane.id = `cdk-overlay-${nextUniqueId++}`;
6593
pane.classList.add('cdk-overlay-pane');
66-
6794
this._overlayContainer.getContainerElement().appendChild(pane);
6895

6996
return pane;
@@ -84,7 +111,28 @@ export class Overlay {
84111
* @param state
85112
*/
86113
private _createOverlayRef(pane: HTMLElement, state: OverlayState): OverlayRef {
87-
return new OverlayRef(this._createPortalHost(pane), pane, state, this._ngZone);
114+
let portalHost = this._createPortalHost(pane);
115+
let scrollStrategy = this._createScrollStrategy(state);
116+
return new OverlayRef(portalHost, pane, state, scrollStrategy, this._ngZone);
117+
}
118+
119+
/**
120+
* Resolves the scroll strategy of an overlay state.
121+
* @param state State for which to resolve the scroll strategy.
122+
*/
123+
private _createScrollStrategy(state: OverlayState): ScrollStrategy {
124+
let strategyName = typeof state.scrollStrategy === 'string' ?
125+
state.scrollStrategy :
126+
state.scrollStrategy.name;
127+
128+
if (!this._scrollStrategies.hasOwnProperty(strategyName)) {
129+
throw new Error(`Unsupported scroll strategy "${strategyName}". The available scroll ` +
130+
`strategies are ${Object.keys(this._scrollStrategies).join(', ')}.`);
131+
}
132+
133+
// Note that we use `resolveAndInstantiate` which will instantiate
134+
// the scroll strategy without putting it in the injector cache.
135+
return this._reflectiveInjector.resolveAndInstantiate(this._scrollStrategies[strategyName]);
88136
}
89137
}
90138

0 commit comments

Comments
 (0)