Skip to content

Commit f0e1273

Browse files
committed
feat(overlay): add basic core of overlay
1 parent 208cd65 commit f0e1273

File tree

11 files changed

+349
-4
lines changed

11 files changed

+349
-4
lines changed

src/core/overlay/overlay-ref.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {PortalHost, Portal} from '../portal/portal';
2+
3+
/**
4+
* Reference to an overlay that has been created with the Overlay service.
5+
* Used to manipulate or dispose of said overlay.
6+
*/
7+
export class OverlayRef implements PortalHost {
8+
constructor(private _portalHost: PortalHost) { }
9+
10+
attach(portal: Portal<any>): Promise<any> {
11+
return this._portalHost.attach(portal);
12+
}
13+
14+
detach(): Promise<any> {
15+
return this._portalHost.detach();
16+
}
17+
18+
dispose(): void {
19+
this._portalHost.dispose();
20+
}
21+
22+
hasAttached(): boolean {
23+
return this._portalHost.hasAttached();
24+
}
25+
26+
// TODO(jelbourn): add additional methods for manipulating the overlay.
27+
}

src/core/overlay/overlay-state.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* OverlayState is a bag of values for either the initial configuration or current state of an
3+
* overlay.
4+
*/
5+
export class OverlayState {
6+
// Not yet implemented.
7+
// TODO(jelbourn): add overlay state / configuration.
8+
}

src/core/overlay/overlay.spec.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import {
2+
inject,
3+
TestComponentBuilder,
4+
fakeAsync,
5+
flushMicrotasks,
6+
beforeEachProviders,
7+
} from 'angular2/testing';
8+
import {
9+
it,
10+
describe,
11+
expect,
12+
beforeEach,
13+
} from '../../core/facade/testing';
14+
import {
15+
Component,
16+
ViewChild,
17+
ElementRef,
18+
provide,
19+
} from 'angular2/core';
20+
import {BrowserDomAdapter} from '../platform/browser/browser_adapter';
21+
import {TemplatePortalDirective} from '../portal/portal-directives';
22+
import {TemplatePortal, ComponentPortal} from '../portal/portal';
23+
import {Overlay, OVERLAY_CONTAINER_TOKEN} from './overlay';
24+
import {DOM} from '../platform/dom/dom_adapter';
25+
import {OverlayRef} from './overlay-ref';
26+
27+
28+
export function main() {
29+
describe('Overlay', () => {
30+
BrowserDomAdapter.makeCurrent();
31+
32+
let builder: TestComponentBuilder;
33+
let overlay: Overlay;
34+
let componentPortal: ComponentPortal;
35+
let templatePortal: TemplatePortal;
36+
let overlayContainerElement: Element;
37+
38+
beforeEachProviders(() => [
39+
Overlay,
40+
provide(OVERLAY_CONTAINER_TOKEN, {useFactory: () => {
41+
overlayContainerElement = DOM.createElement('div');
42+
return overlayContainerElement;
43+
}})
44+
]);
45+
46+
let deps = [TestComponentBuilder, Overlay];
47+
beforeEach(inject(deps, fakeAsync((tcb: TestComponentBuilder, o: Overlay) => {
48+
builder = tcb;
49+
overlay = o;
50+
51+
builder.createAsync(TestComponentWithTemplatePortals).then(fixture => {
52+
fixture.detectChanges();
53+
templatePortal = fixture.componentInstance.templatePortal;
54+
componentPortal = new ComponentPortal(PizzaMsg, fixture.componentInstance.elementRef);
55+
});
56+
57+
flushMicrotasks();
58+
})));
59+
60+
it('should load a component into an overlay', fakeAsyncTest(() => {
61+
let overlayRef: OverlayRef;
62+
63+
overlay.create().then(ref => {
64+
overlayRef = ref;
65+
overlayRef.attach(componentPortal);
66+
});
67+
68+
flushMicrotasks();
69+
70+
expect(overlayContainerElement.textContent).toContain('Pizza');
71+
72+
overlayRef.dispose();
73+
expect(overlayContainerElement.childNodes.length).toBe(0);
74+
expect(overlayContainerElement.textContent).toBe('');
75+
}));
76+
77+
it('should load a template portal into an overlay', fakeAsyncTest(() => {
78+
let overlayRef: OverlayRef;
79+
80+
overlay.create().then(ref => {
81+
overlayRef = ref;
82+
overlayRef.attach(templatePortal);
83+
});
84+
85+
flushMicrotasks();
86+
87+
expect(overlayContainerElement.textContent).toContain('Cake');
88+
89+
overlayRef.dispose();
90+
expect(overlayContainerElement.childNodes.length).toBe(0);
91+
expect(overlayContainerElement.textContent).toBe('');
92+
}));
93+
94+
it('should open multiple overlays', fakeAsyncTest(() => {
95+
let pizzaOverlayRef: OverlayRef;
96+
let cakeOverlayRef: OverlayRef;
97+
98+
overlay.create().then(ref => {
99+
pizzaOverlayRef = ref;
100+
pizzaOverlayRef.attach(componentPortal);
101+
});
102+
103+
flushMicrotasks();
104+
105+
overlay.create().then(ref => {
106+
cakeOverlayRef = ref;
107+
cakeOverlayRef.attach(templatePortal);
108+
});
109+
110+
flushMicrotasks();
111+
112+
expect(overlayContainerElement.childNodes.length).toBe(2);
113+
expect(overlayContainerElement.textContent).toContain('Pizza');
114+
expect(overlayContainerElement.textContent).toContain('Cake');
115+
116+
pizzaOverlayRef.dispose();
117+
expect(overlayContainerElement.childNodes.length).toBe(1);
118+
expect(overlayContainerElement.textContent).toContain('Cake');
119+
120+
cakeOverlayRef.dispose();
121+
expect(overlayContainerElement.childNodes.length).toBe(0);
122+
expect(overlayContainerElement.textContent).toBe('');
123+
}));
124+
});
125+
}
126+
127+
128+
/** Simple component for testing ComponentPortal. */
129+
@Component({
130+
selector: 'pizza-msg',
131+
template: '<p>Pizza</p>',
132+
})
133+
class PizzaMsg {}
134+
135+
136+
/** Test-bed component that contains a TempatePortal and an ElementRef. */
137+
@Component({
138+
selector: 'portal-test',
139+
template: `<template portal>Cake</template>`,
140+
directives: [TemplatePortalDirective],
141+
})
142+
class TestComponentWithTemplatePortals {
143+
@ViewChild(TemplatePortalDirective) templatePortal: TemplatePortalDirective;
144+
constructor(public elementRef: ElementRef) { }
145+
}
146+
147+
function fakeAsyncTest(fn: () => void) {
148+
return inject([], fakeAsync(fn));
149+
}

src/core/overlay/overlay.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
DynamicComponentLoader,
3+
AppViewManager,
4+
OpaqueToken,
5+
Inject,
6+
Injectable} from 'angular2/core';
7+
import {CONST_EXPR} from 'angular2/src/facade/lang';
8+
import {OverlayState} from './overlay-state';
9+
import {DomPortalHost} from '../portal/dom-portal-host';
10+
import {OverlayRef} from './overlay-ref';
11+
import {DOM} from '../platform/dom/dom_adapter';
12+
13+
// Re-export OverlayState and OverlayRef so they can be imported directly from here.
14+
export {OverlayState} from './overlay-state';
15+
export {OverlayRef} from './overlay-ref';
16+
17+
/** Token used to inject the DOM element that serves as the overlay container. */
18+
export const OVERLAY_CONTAINER_TOKEN = CONST_EXPR(new OpaqueToken('overlayContainer'));
19+
20+
/** Next overlay unique ID. */
21+
let nextUniqueId = 0;
22+
23+
/** The default state for newly created overlays. */
24+
let defaultState = new OverlayState();
25+
26+
27+
/**
28+
* Service to create Overlays. Overlays are dynamically added pieces of floating UI, meant to be
29+
* used as a low-level building building block for other components. Dialogs, tooltips, menus,
30+
* selects, etc. can all be built using overlays. The service should primarily be used by authors
31+
* of re-usable components rather than developers building end-user applications.
32+
*
33+
* An overlay *is* a PortalHost, so any kind of Portal can be loaded into one.
34+
*/
35+
@Injectable()
36+
export class Overlay {
37+
constructor(
38+
@Inject(OVERLAY_CONTAINER_TOKEN) private _overlayContainerElement: Element,
39+
private _dynamicComponentLoader: DynamicComponentLoader,
40+
private _appViewManager: AppViewManager) {
41+
}
42+
43+
/**
44+
* Creates an overlay.
45+
* @param state State to apply to the overlay.
46+
* @returns A reference to the created overlay.
47+
*/
48+
create(state: OverlayState = defaultState): Promise<OverlayRef> {
49+
return this._createPaneElement(state).then(pane => this._createOverlayRef(pane));
50+
}
51+
52+
/**
53+
* Creates the DOM element for an overlay.
54+
* @param state State to apply to the created element.
55+
* @returns Promise resolving to the created element.
56+
*/
57+
private _createPaneElement(state: OverlayState): Promise<Element> {
58+
var pane = DOM.createElement('div');
59+
pane.id = `md-overlay-${nextUniqueId++}`;
60+
DOM.addClass(pane, 'md-overlay-pane');
61+
62+
this.applyState(pane, state);
63+
this._overlayContainerElement.appendChild(pane);
64+
65+
return Promise.resolve(pane);
66+
}
67+
68+
/**
69+
* Applies a given state to the given pane element.
70+
* @param pane The pane to modify.
71+
* @param state The state to apply.
72+
*/
73+
applyState(pane: Element, state: OverlayState) {
74+
// Not yet implemented.
75+
// TODO(jelbourn): apply state to the pane element.
76+
}
77+
78+
/**
79+
* Create a DomPortalHost into which the overlay content can be loaded.
80+
* @param pane The DOM element to turn into a portal host.
81+
* @returns A portal host for the given DOM element.
82+
*/
83+
private _createPortalHost(pane: Element): DomPortalHost {
84+
return new DomPortalHost(
85+
pane,
86+
this._dynamicComponentLoader,
87+
this._appViewManager);
88+
}
89+
90+
/**
91+
* Creates an OverlayRef for an overlay in the given DOM element.
92+
* @param pane DOM element for the overlay
93+
* @returns {OverlayRef}
94+
*/
95+
private _createOverlayRef(pane: Element): OverlayRef {
96+
return new OverlayRef(this._createPortalHost(pane));
97+
}
98+
}

src/core/portal/dom-portal-host.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,11 @@ export class DomPortalHost extends BasePortalHost {
5151
// TODO(jelbourn): Return locals from view.
5252
return Promise.resolve(new Map<string, any>());
5353
}
54+
55+
dispose(): void {
56+
super.dispose();
57+
if (this._hostDomElement.parentNode != null) {
58+
this._hostDomElement.parentNode.removeChild(this._hostDomElement);
59+
}
60+
}
5461
}

src/demo-app/demo-app.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ <h1>Angular Material2 Demos</h1>
66
<li><a [routerLink]="['SidenavDemo']">Sidenav demo</a></li>
77
<li><a [routerLink]="['ProgressCircleDemo']">Progress Circle demo</a></li>
88
<li><a [routerLink]="['PortalDemo']">Portal demo</a></li>
9+
<li><a [routerLink]="['OverlayDemo']">Overlay demo</a></li>
910
<li><a [routerLink]="['CheckboxDemo']">Checkbox demo</a></li>
1011
<li><a [routerLink]="['ToolbarDemo']">Toolbar demo</a></li>
1112
<li><a [routerLink]="['RadioDemo']">Radio demo</a></li>

src/demo-app/demo-app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {Dir} from '../core/rtl/dir';
1010
import {MdButton} from '../components/button/button';
1111
import {PortalDemo} from './portal/portal-demo';
1212
import {ToolbarDemo} from './toolbar/toolbar-demo';
13+
import {OverlayDemo} from './overlay/overlay-demo';
1314
import {ListDemo} from './list/list-demo';
1415

1516
@Component({
@@ -34,6 +35,7 @@ export class Home {}
3435
new Route({path: '/sidenav', name: 'SidenavDemo', component: SidenavDemo}),
3536
new Route({path: '/progress-circle', name: 'ProgressCircleDemo', component: ProgressCircleDemo}),
3637
new Route({path: '/portal', name: 'PortalDemo', component: PortalDemo}),
38+
new Route({path: '/overlay', name: 'OverlayDemo', component: OverlayDemo}),
3739
new Route({path: '/checkbox', name: 'CheckboxDemo', component: CheckboxDemo}),
3840
new Route({path: '/toolbar', name: 'ToolbarDemo', component: ToolbarDemo}),
3941
new Route({path: '/list', name: 'ListDemo', component: ListDemo})
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<button (click)="openRotiniPanel()">
2+
Pasta 1
3+
</button>
4+
5+
<button (click)="openFusilliPanel()">
6+
Pasta 2
7+
</button>
8+
9+
<!-- Template to load into an overlay. -->
10+
<template portal>
11+
<p> Fusilli </p>
12+
</template>

src/demo-app/overlay/overlay-demo.scss

Whitespace-only changes.

src/demo-app/overlay/overlay-demo.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {Component, provide, ElementRef, ViewChildren, QueryList} from 'angular2/core';
2+
import {Overlay, OVERLAY_CONTAINER_TOKEN} from '../../core/overlay/overlay';
3+
import {ComponentPortal, Portal} from '../../core/portal/portal';
4+
import {BrowserDomAdapter} from '../../core/platform/browser/browser_adapter';
5+
import {TemplatePortalDirective} from '../../core/portal/portal-directives';
6+
7+
8+
@Component({
9+
selector: 'overlay-demo',
10+
templateUrl: 'demo-app/overlay/overlay-demo.html',
11+
styleUrls: ['demo-app/overlay/overlay-demo.css'],
12+
directives: [TemplatePortalDirective],
13+
providers: [
14+
Overlay,
15+
provide(OVERLAY_CONTAINER_TOKEN, {useValue: document.body})
16+
]
17+
})
18+
export class OverlayDemo {
19+
@ViewChildren(TemplatePortalDirective) templatePortals: QueryList<Portal<any>>;
20+
21+
constructor(public overlay: Overlay, public elementRef: ElementRef) {
22+
BrowserDomAdapter.makeCurrent();
23+
}
24+
25+
openRotiniPanel() {
26+
this.overlay.create().then(ref => {
27+
ref.attach(new ComponentPortal(PastaPanel, this.elementRef));
28+
});
29+
}
30+
31+
openFusilliPanel() {
32+
this.overlay.create().then(ref => {
33+
ref.attach(this.templatePortals.first);
34+
});
35+
}
36+
}
37+
38+
/** Simple component to load into an overlay */
39+
@Component({
40+
selector: 'pasta-panel',
41+
template: '<p>Rotini {{value}}</p>'
42+
})
43+
class PastaPanel {
44+
value: number = 9000;
45+
}

src/demo-app/portal/portal-demo.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ export class PortalDemo {
1717

1818
selectedPortal: Portal<any>;
1919

20-
constructor() {
21-
console.log('~~ contructor ~~');
22-
}
23-
2420
get programmingJoke() {
2521
return this.templatePortals.first;
2622
}

0 commit comments

Comments
 (0)