Skip to content

Commit

Permalink
feat(overlay): add basic core of overlay
Browse files Browse the repository at this point in the history
  • Loading branch information
jelbourn committed Mar 22, 2016
1 parent 208cd65 commit f0e1273
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 4 deletions.
27 changes: 27 additions & 0 deletions src/core/overlay/overlay-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {PortalHost, Portal} from '../portal/portal';

/**
* Reference to an overlay that has been created with the Overlay service.
* Used to manipulate or dispose of said overlay.
*/
export class OverlayRef implements PortalHost {
constructor(private _portalHost: PortalHost) { }

attach(portal: Portal<any>): Promise<any> {
return this._portalHost.attach(portal);
}

detach(): Promise<any> {
return this._portalHost.detach();
}

dispose(): void {
this._portalHost.dispose();
}

hasAttached(): boolean {
return this._portalHost.hasAttached();
}

// TODO(jelbourn): add additional methods for manipulating the overlay.
}
8 changes: 8 additions & 0 deletions src/core/overlay/overlay-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* OverlayState is a bag of values for either the initial configuration or current state of an
* overlay.
*/
export class OverlayState {
// Not yet implemented.
// TODO(jelbourn): add overlay state / configuration.
}
149 changes: 149 additions & 0 deletions src/core/overlay/overlay.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import {
inject,
TestComponentBuilder,
fakeAsync,
flushMicrotasks,
beforeEachProviders,
} from 'angular2/testing';
import {
it,
describe,
expect,
beforeEach,
} from '../../core/facade/testing';
import {
Component,
ViewChild,
ElementRef,
provide,
} from 'angular2/core';
import {BrowserDomAdapter} from '../platform/browser/browser_adapter';
import {TemplatePortalDirective} from '../portal/portal-directives';
import {TemplatePortal, ComponentPortal} from '../portal/portal';
import {Overlay, OVERLAY_CONTAINER_TOKEN} from './overlay';
import {DOM} from '../platform/dom/dom_adapter';
import {OverlayRef} from './overlay-ref';


export function main() {
describe('Overlay', () => {
BrowserDomAdapter.makeCurrent();

let builder: TestComponentBuilder;
let overlay: Overlay;
let componentPortal: ComponentPortal;
let templatePortal: TemplatePortal;
let overlayContainerElement: Element;

beforeEachProviders(() => [
Overlay,
provide(OVERLAY_CONTAINER_TOKEN, {useFactory: () => {
overlayContainerElement = DOM.createElement('div');
return overlayContainerElement;
}})
]);

let deps = [TestComponentBuilder, Overlay];
beforeEach(inject(deps, fakeAsync((tcb: TestComponentBuilder, o: Overlay) => {
builder = tcb;
overlay = o;

builder.createAsync(TestComponentWithTemplatePortals).then(fixture => {
fixture.detectChanges();
templatePortal = fixture.componentInstance.templatePortal;
componentPortal = new ComponentPortal(PizzaMsg, fixture.componentInstance.elementRef);
});

flushMicrotasks();
})));

it('should load a component into an overlay', fakeAsyncTest(() => {
let overlayRef: OverlayRef;

overlay.create().then(ref => {
overlayRef = ref;
overlayRef.attach(componentPortal);
});

flushMicrotasks();

expect(overlayContainerElement.textContent).toContain('Pizza');

overlayRef.dispose();
expect(overlayContainerElement.childNodes.length).toBe(0);
expect(overlayContainerElement.textContent).toBe('');
}));

it('should load a template portal into an overlay', fakeAsyncTest(() => {
let overlayRef: OverlayRef;

overlay.create().then(ref => {
overlayRef = ref;
overlayRef.attach(templatePortal);
});

flushMicrotasks();

expect(overlayContainerElement.textContent).toContain('Cake');

overlayRef.dispose();
expect(overlayContainerElement.childNodes.length).toBe(0);
expect(overlayContainerElement.textContent).toBe('');
}));

it('should open multiple overlays', fakeAsyncTest(() => {
let pizzaOverlayRef: OverlayRef;
let cakeOverlayRef: OverlayRef;

overlay.create().then(ref => {
pizzaOverlayRef = ref;
pizzaOverlayRef.attach(componentPortal);
});

flushMicrotasks();

overlay.create().then(ref => {
cakeOverlayRef = ref;
cakeOverlayRef.attach(templatePortal);
});

flushMicrotasks();

expect(overlayContainerElement.childNodes.length).toBe(2);
expect(overlayContainerElement.textContent).toContain('Pizza');
expect(overlayContainerElement.textContent).toContain('Cake');

pizzaOverlayRef.dispose();
expect(overlayContainerElement.childNodes.length).toBe(1);
expect(overlayContainerElement.textContent).toContain('Cake');

cakeOverlayRef.dispose();
expect(overlayContainerElement.childNodes.length).toBe(0);
expect(overlayContainerElement.textContent).toBe('');
}));
});
}


/** Simple component for testing ComponentPortal. */
@Component({
selector: 'pizza-msg',
template: '<p>Pizza</p>',
})
class PizzaMsg {}


/** Test-bed component that contains a TempatePortal and an ElementRef. */
@Component({
selector: 'portal-test',
template: `<template portal>Cake</template>`,
directives: [TemplatePortalDirective],
})
class TestComponentWithTemplatePortals {
@ViewChild(TemplatePortalDirective) templatePortal: TemplatePortalDirective;
constructor(public elementRef: ElementRef) { }
}

function fakeAsyncTest(fn: () => void) {
return inject([], fakeAsync(fn));
}
98 changes: 98 additions & 0 deletions src/core/overlay/overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
DynamicComponentLoader,
AppViewManager,
OpaqueToken,
Inject,
Injectable} from 'angular2/core';
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {OverlayState} from './overlay-state';
import {DomPortalHost} from '../portal/dom-portal-host';
import {OverlayRef} from './overlay-ref';
import {DOM} from '../platform/dom/dom_adapter';

// Re-export OverlayState and OverlayRef so they can be imported directly from here.
export {OverlayState} from './overlay-state';
export {OverlayRef} from './overlay-ref';

/** Token used to inject the DOM element that serves as the overlay container. */
export const OVERLAY_CONTAINER_TOKEN = CONST_EXPR(new OpaqueToken('overlayContainer'));

/** Next overlay unique ID. */
let nextUniqueId = 0;

/** The default state for newly created overlays. */
let defaultState = new OverlayState();


/**
* Service to create Overlays. Overlays are dynamically added pieces of floating UI, meant to be
* used as a low-level building building block for other components. Dialogs, tooltips, menus,
* selects, etc. can all be built using overlays. The service should primarily be used by authors
* of re-usable components rather than developers building end-user applications.
*
* An overlay *is* a PortalHost, so any kind of Portal can be loaded into one.
*/
@Injectable()
export class Overlay {
constructor(
@Inject(OVERLAY_CONTAINER_TOKEN) private _overlayContainerElement: Element,
private _dynamicComponentLoader: DynamicComponentLoader,
private _appViewManager: AppViewManager) {
}

/**
* Creates an overlay.
* @param state State to apply to the overlay.
* @returns A reference to the created overlay.
*/
create(state: OverlayState = defaultState): Promise<OverlayRef> {
return this._createPaneElement(state).then(pane => this._createOverlayRef(pane));
}

/**
* Creates the DOM element for an overlay.
* @param state State to apply to the created element.
* @returns Promise resolving to the created element.
*/
private _createPaneElement(state: OverlayState): Promise<Element> {
var pane = DOM.createElement('div');
pane.id = `md-overlay-${nextUniqueId++}`;
DOM.addClass(pane, 'md-overlay-pane');

this.applyState(pane, state);
this._overlayContainerElement.appendChild(pane);

return Promise.resolve(pane);
}

/**
* Applies a given state to the given pane element.
* @param pane The pane to modify.
* @param state The state to apply.
*/
applyState(pane: Element, state: OverlayState) {
// Not yet implemented.
// TODO(jelbourn): apply state to the pane element.
}

/**
* Create a DomPortalHost into which the overlay content can be loaded.
* @param pane The DOM element to turn into a portal host.
* @returns A portal host for the given DOM element.
*/
private _createPortalHost(pane: Element): DomPortalHost {
return new DomPortalHost(
pane,
this._dynamicComponentLoader,
this._appViewManager);
}

/**
* Creates an OverlayRef for an overlay in the given DOM element.
* @param pane DOM element for the overlay
* @returns {OverlayRef}
*/
private _createOverlayRef(pane: Element): OverlayRef {
return new OverlayRef(this._createPortalHost(pane));
}
}
7 changes: 7 additions & 0 deletions src/core/portal/dom-portal-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,11 @@ export class DomPortalHost extends BasePortalHost {
// TODO(jelbourn): Return locals from view.
return Promise.resolve(new Map<string, any>());
}

dispose(): void {
super.dispose();
if (this._hostDomElement.parentNode != null) {
this._hostDomElement.parentNode.removeChild(this._hostDomElement);
}
}
}
1 change: 1 addition & 0 deletions src/demo-app/demo-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ <h1>Angular Material2 Demos</h1>
<li><a [routerLink]="['SidenavDemo']">Sidenav demo</a></li>
<li><a [routerLink]="['ProgressCircleDemo']">Progress Circle demo</a></li>
<li><a [routerLink]="['PortalDemo']">Portal demo</a></li>
<li><a [routerLink]="['OverlayDemo']">Overlay demo</a></li>
<li><a [routerLink]="['CheckboxDemo']">Checkbox demo</a></li>
<li><a [routerLink]="['ToolbarDemo']">Toolbar demo</a></li>
<li><a [routerLink]="['RadioDemo']">Radio demo</a></li>
Expand Down
2 changes: 2 additions & 0 deletions src/demo-app/demo-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {Dir} from '../core/rtl/dir';
import {MdButton} from '../components/button/button';
import {PortalDemo} from './portal/portal-demo';
import {ToolbarDemo} from './toolbar/toolbar-demo';
import {OverlayDemo} from './overlay/overlay-demo';
import {ListDemo} from './list/list-demo';

@Component({
Expand All @@ -34,6 +35,7 @@ export class Home {}
new Route({path: '/sidenav', name: 'SidenavDemo', component: SidenavDemo}),
new Route({path: '/progress-circle', name: 'ProgressCircleDemo', component: ProgressCircleDemo}),
new Route({path: '/portal', name: 'PortalDemo', component: PortalDemo}),
new Route({path: '/overlay', name: 'OverlayDemo', component: OverlayDemo}),
new Route({path: '/checkbox', name: 'CheckboxDemo', component: CheckboxDemo}),
new Route({path: '/toolbar', name: 'ToolbarDemo', component: ToolbarDemo}),
new Route({path: '/list', name: 'ListDemo', component: ListDemo})
Expand Down
12 changes: 12 additions & 0 deletions src/demo-app/overlay/overlay-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<button (click)="openRotiniPanel()">
Pasta 1
</button>

<button (click)="openFusilliPanel()">
Pasta 2
</button>

<!-- Template to load into an overlay. -->
<template portal>
<p> Fusilli </p>
</template>
Empty file.
45 changes: 45 additions & 0 deletions src/demo-app/overlay/overlay-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {Component, provide, ElementRef, ViewChildren, QueryList} from 'angular2/core';
import {Overlay, OVERLAY_CONTAINER_TOKEN} from '../../core/overlay/overlay';
import {ComponentPortal, Portal} from '../../core/portal/portal';
import {BrowserDomAdapter} from '../../core/platform/browser/browser_adapter';
import {TemplatePortalDirective} from '../../core/portal/portal-directives';


@Component({
selector: 'overlay-demo',
templateUrl: 'demo-app/overlay/overlay-demo.html',
styleUrls: ['demo-app/overlay/overlay-demo.css'],
directives: [TemplatePortalDirective],
providers: [
Overlay,
provide(OVERLAY_CONTAINER_TOKEN, {useValue: document.body})
]
})
export class OverlayDemo {
@ViewChildren(TemplatePortalDirective) templatePortals: QueryList<Portal<any>>;

constructor(public overlay: Overlay, public elementRef: ElementRef) {
BrowserDomAdapter.makeCurrent();
}

openRotiniPanel() {
this.overlay.create().then(ref => {
ref.attach(new ComponentPortal(PastaPanel, this.elementRef));
});
}

openFusilliPanel() {
this.overlay.create().then(ref => {
ref.attach(this.templatePortals.first);
});
}
}

/** Simple component to load into an overlay */
@Component({
selector: 'pasta-panel',
template: '<p>Rotini {{value}}</p>'
})
class PastaPanel {
value: number = 9000;
}
4 changes: 0 additions & 4 deletions src/demo-app/portal/portal-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ export class PortalDemo {

selectedPortal: Portal<any>;

constructor() {
console.log('~~ contructor ~~');
}

get programmingJoke() {
return this.templatePortals.first;
}
Expand Down

0 comments on commit f0e1273

Please sign in to comment.