mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(overlay): add basic core of overlay
This commit is contained in:
@@ -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.
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<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>
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -17,10 +17,6 @@ export class PortalDemo {
|
||||
|
||||
selectedPortal: Portal<any>;
|
||||
|
||||
constructor() {
|
||||
console.log('~~ contructor ~~');
|
||||
}
|
||||
|
||||
get programmingJoke() {
|
||||
return this.templatePortals.first;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user