diff --git a/src/core/overlay/overlay-ref.ts b/src/core/overlay/overlay-ref.ts new file mode 100644 index 000000000000..ce9fb46b1476 --- /dev/null +++ b/src/core/overlay/overlay-ref.ts @@ -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): Promise { + return this._portalHost.attach(portal); + } + + detach(): Promise { + return this._portalHost.detach(); + } + + dispose(): void { + this._portalHost.dispose(); + } + + hasAttached(): boolean { + return this._portalHost.hasAttached(); + } + + // TODO(jelbourn): add additional methods for manipulating the overlay. +} diff --git a/src/core/overlay/overlay-state.ts b/src/core/overlay/overlay-state.ts new file mode 100644 index 000000000000..712c8e9ffcbb --- /dev/null +++ b/src/core/overlay/overlay-state.ts @@ -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. +} diff --git a/src/core/overlay/overlay.spec.ts b/src/core/overlay/overlay.spec.ts new file mode 100644 index 000000000000..a4c000101ee1 --- /dev/null +++ b/src/core/overlay/overlay.spec.ts @@ -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: '

Pizza

', +}) +class PizzaMsg {} + + +/** Test-bed component that contains a TempatePortal and an ElementRef. */ +@Component({ + selector: 'portal-test', + template: ``, + directives: [TemplatePortalDirective], +}) +class TestComponentWithTemplatePortals { + @ViewChild(TemplatePortalDirective) templatePortal: TemplatePortalDirective; + constructor(public elementRef: ElementRef) { } +} + +function fakeAsyncTest(fn: () => void) { + return inject([], fakeAsync(fn)); +} diff --git a/src/core/overlay/overlay.ts b/src/core/overlay/overlay.ts new file mode 100644 index 000000000000..1e20c4503269 --- /dev/null +++ b/src/core/overlay/overlay.ts @@ -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 { + 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 { + 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)); + } +} diff --git a/src/core/portal/dom-portal-host.ts b/src/core/portal/dom-portal-host.ts index 90bd9709d85f..3e87d545e1a6 100644 --- a/src/core/portal/dom-portal-host.ts +++ b/src/core/portal/dom-portal-host.ts @@ -51,4 +51,11 @@ export class DomPortalHost extends BasePortalHost { // TODO(jelbourn): Return locals from view. return Promise.resolve(new Map()); } + + dispose(): void { + super.dispose(); + if (this._hostDomElement.parentNode != null) { + this._hostDomElement.parentNode.removeChild(this._hostDomElement); + } + } } diff --git a/src/demo-app/demo-app.html b/src/demo-app/demo-app.html index 0ec37dfec0c3..6abc9c03925d 100644 --- a/src/demo-app/demo-app.html +++ b/src/demo-app/demo-app.html @@ -6,6 +6,7 @@

Angular Material2 Demos

  • Sidenav demo
  • Progress Circle demo
  • Portal demo
  • +
  • Overlay demo
  • Checkbox demo
  • Toolbar demo
  • Radio demo
  • diff --git a/src/demo-app/demo-app.ts b/src/demo-app/demo-app.ts index b7219f666aa8..7bb3eb28573d 100644 --- a/src/demo-app/demo-app.ts +++ b/src/demo-app/demo-app.ts @@ -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({ @@ -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}) diff --git a/src/demo-app/overlay/overlay-demo.html b/src/demo-app/overlay/overlay-demo.html new file mode 100644 index 000000000000..33c1c01499d2 --- /dev/null +++ b/src/demo-app/overlay/overlay-demo.html @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/demo-app/overlay/overlay-demo.scss b/src/demo-app/overlay/overlay-demo.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/demo-app/overlay/overlay-demo.ts b/src/demo-app/overlay/overlay-demo.ts new file mode 100644 index 000000000000..63679fdb68fd --- /dev/null +++ b/src/demo-app/overlay/overlay-demo.ts @@ -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>; + + 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: '

    Rotini {{value}}

    ' +}) +class PastaPanel { + value: number = 9000; +} diff --git a/src/demo-app/portal/portal-demo.ts b/src/demo-app/portal/portal-demo.ts index 9fe95ba32411..2ea7d81adf00 100644 --- a/src/demo-app/portal/portal-demo.ts +++ b/src/demo-app/portal/portal-demo.ts @@ -17,10 +17,6 @@ export class PortalDemo { selectedPortal: Portal; - constructor() { - console.log('~~ contructor ~~'); - } - get programmingJoke() { return this.templatePortals.first; }