mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(portal): add initial portal foundation
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
# Portals
|
||||
|
||||
### Overview
|
||||
|
||||
A `Portal `is a piece of UI that can be dynamically rendered to an open slot on the page.
|
||||
|
||||
The "piece of UI" can be either a `Component` or a `TemplateRef`.
|
||||
|
||||
The "open slot" is a `PortalHost`.
|
||||
|
||||
Portals and PortalHosts are low-level building blocks that other concepts, such as overlays, can
|
||||
be built upon.
|
||||
|
||||
##### `Portal<T>`
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `attach(PortalHost): Promise<T>` | Attaches the portal to a host. |
|
||||
| `detach(): Promise<void>` | Detaches the portal from its host. |
|
||||
| `isAttached: boolean` | Whether the portal is attached. |
|
||||
|
||||
##### `PortalHost`
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `attach(Portal): Promise<void>` | Attaches a portal to the host. |
|
||||
| `detach(): Promise<void>` | Detaches the portal from the host. |
|
||||
| `dispose(): Promise<void>` | Permanently dispose the host. |
|
||||
| `hasAttached: boolean` | Whether a portal is attached to the host. |
|
||||
|
||||
|
||||
|
||||
|
||||
### Using portals
|
||||
|
||||
|
||||
|
||||
##### `TemplatePortalDirective`
|
||||
Used to get a portal from a `<template>`. `TemplatePortalDirectives` *is* a `Portal`.
|
||||
|
||||
Usage:
|
||||
```html
|
||||
<template portal>
|
||||
<p>The content of this template is captured by the portal.</p>
|
||||
</template>
|
||||
|
||||
<!-- OR -->
|
||||
|
||||
<!-- This result here is identical to the syntax above -->
|
||||
<p *portal>
|
||||
The content of this template is captured by the portal.
|
||||
</p>
|
||||
```
|
||||
|
||||
A component can use `@ViewChild` or `@ViewChildren` to get a reference to a
|
||||
`TemplatePortalDiective`.
|
||||
|
||||
##### `ComponentPortal`
|
||||
Used to create a portal from a component type.
|
||||
|
||||
Usage:
|
||||
```ts
|
||||
this.userSettingsPortal = new ComponentPortal(UserSettingsComponent);
|
||||
```
|
||||
|
||||
|
||||
##### `PortalHostDirective`
|
||||
Used to add a portal host to a template. `PortalHostDirective` *is* a `PortalHost`.
|
||||
|
||||
Usage:
|
||||
```html
|
||||
<!-- Attaches the `userSettingsPortal` from the previous example. -->
|
||||
<template [portalHost]="userSettingsPortal"></template>
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
import {Portal} from './portal';
|
||||
import {TemplatePortal} from './portal';
|
||||
import {ComponentRef} from 'angular2/core';
|
||||
import {ComponentPortal} from './portal';
|
||||
import {Directive} from 'angular2/core';
|
||||
import {TemplateRef} from 'angular2/core';
|
||||
import {BasePortalHost} from './portal';
|
||||
import {DynamicComponentLoader} from 'angular2/core';
|
||||
import {ElementRef} from 'angular2/core';
|
||||
import {ViewContainerRef} from 'angular2/core';
|
||||
|
||||
|
||||
/**
|
||||
* Directive version of a `TemplatePortal`. Because the directive *is* a TemplatePortal,
|
||||
* the directive instance itself can be attached to a host, enabling declarative use of portals.
|
||||
*
|
||||
* Usage:
|
||||
* <template portal #greeting>
|
||||
* <p> Hello {{name}} </p>
|
||||
* </template>
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[portal]',
|
||||
exportAs: 'portal',
|
||||
})
|
||||
export class TemplatePortalDirective extends TemplatePortal {
|
||||
constructor(templateRef: TemplateRef) {
|
||||
super(templateRef);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Directive version of a PortalHost. Because the directive *is* a PortalHost, portals can be
|
||||
* directly attached to it, enabling declarative use.
|
||||
*
|
||||
* Usage:
|
||||
* <template [portalHost]="greeting"></template>
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[portalHost]',
|
||||
inputs: ['portal: portalHost']
|
||||
})
|
||||
export class PortalHostDirective extends BasePortalHost {
|
||||
/** The attached portal. */
|
||||
private portal_: Portal<any>;
|
||||
|
||||
constructor(
|
||||
private dynamicComponentLoader_: DynamicComponentLoader,
|
||||
private elementRef_: ElementRef,
|
||||
private viewContainerRef_: ViewContainerRef) {
|
||||
super();
|
||||
}
|
||||
|
||||
get portal(): Portal<any> {
|
||||
return this.portal_;
|
||||
}
|
||||
|
||||
set portal(p: Portal<any>) {
|
||||
this.replaceAttachedPortal_(p);
|
||||
}
|
||||
|
||||
/** Attach the given ComponentPortal to this PortlHost using the DynamicComponentLoader. */
|
||||
attachComponentPortal(portal: ComponentPortal): Promise<ComponentRef> {
|
||||
portal.setAttachedHost(this);
|
||||
|
||||
// If the portal specifies an origin, use that as the logical location of the component
|
||||
// in the application tree. Otherwise use the location of this PortalHost.
|
||||
let elementRef = portal.origin != null ? portal.origin : this.elementRef_;
|
||||
|
||||
// Typecast is necessary for Dart transpilation.
|
||||
return <Promise<ComponentRef>>
|
||||
this.dynamicComponentLoader_.loadNextToLocation(portal.component, elementRef)
|
||||
.then(ref => {
|
||||
this.setDisposeFn(() => ref.dispose());
|
||||
return ref;
|
||||
});
|
||||
}
|
||||
|
||||
/** Attach the given TemplatePortal to this PortlHost as an embedded View. */
|
||||
attachTemplatePortal(portal: TemplatePortal): Promise<Map<string, any>> {
|
||||
portal.setAttachedHost(this);
|
||||
|
||||
let viewRef = this.viewContainerRef_.createEmbeddedView(portal.templateRef);
|
||||
portal.locals.forEach((v, k) => viewRef.setLocal(k, v));
|
||||
this.setDisposeFn(() => this.viewContainerRef_.clear());
|
||||
|
||||
// TODO(jelbourn): return locals from view
|
||||
// Typecast is necessary for Dart transpilation.
|
||||
return <Promise<Map<string, any>>> Promise.resolve(new Map<string, any>());
|
||||
}
|
||||
|
||||
/** Detatches the currently attached Portal (if there is one) and attaches the given Portal. */
|
||||
private replaceAttachedPortal_(p: Portal<any>): void {
|
||||
let maybeDetach = this.hasAttached() ? this.detach() : Promise.resolve(null);
|
||||
|
||||
maybeDetach.then(_ => {
|
||||
if (p != null) {
|
||||
this.attach(p);
|
||||
this.portal_ = p;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import {
|
||||
inject,
|
||||
ComponentFixture,
|
||||
TestComponentBuilder,
|
||||
} from 'angular2/testing';
|
||||
import {
|
||||
it,
|
||||
iit,
|
||||
describe,
|
||||
ddescribe,
|
||||
expect,
|
||||
beforeEach,
|
||||
} from '../../core/facade/testing';
|
||||
import {Component, ViewChildren, QueryList} from 'angular2/core';
|
||||
import {TemplatePortalDirective} from './portal-directives';
|
||||
import {Portal} from './portal';
|
||||
import {ComponentPortal} from './portal';
|
||||
import {PortalHostDirective} from './portal-directives';
|
||||
import {fakeAsync} from 'angular2/testing';
|
||||
import {flushMicrotasks} from 'angular2/testing';
|
||||
|
||||
|
||||
export function main() {
|
||||
describe('Portal directives', () => {
|
||||
let builder: TestComponentBuilder;
|
||||
|
||||
beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
builder = tcb;
|
||||
}));
|
||||
|
||||
it('should load a component into the portal', fakeAsyncTest(() => {
|
||||
let appFixture: ComponentFixture;
|
||||
|
||||
builder.createAsync(PortalTestApp).then(fixture => {
|
||||
appFixture = fixture;
|
||||
});
|
||||
|
||||
// Flush the async creation of the PortalTestApp.
|
||||
flushMicrotasks();
|
||||
|
||||
// Set the selectedHost to be a ComponentPortal.
|
||||
let testAppComponent = appFixture.debugElement.componentInstance;
|
||||
testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg);
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Flush the attachment of the Portal.
|
||||
flushMicrotasks();
|
||||
|
||||
// Expect that the content of the attached portal is present.
|
||||
let hostContainer = appFixture.nativeElement.querySelector('.portal-container');
|
||||
expect(hostContainer.textContent).toContain('Pizza');
|
||||
}));
|
||||
|
||||
it('should load a <template> portal', fakeAsyncTest(() => {
|
||||
let appFixture: ComponentFixture;
|
||||
|
||||
builder.createAsync(PortalTestApp).then(fixture => {
|
||||
appFixture = fixture;
|
||||
});
|
||||
|
||||
// Flush the async creation of the PortalTestApp.
|
||||
flushMicrotasks();
|
||||
|
||||
let testAppComponent = appFixture.debugElement.componentInstance;
|
||||
|
||||
// Detect changes initially so that the component's ViewChildren are resolved.
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Set the selectedHost to be a TemplatePortal.
|
||||
testAppComponent.selectedPortal = testAppComponent.cakePortal;
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Flush the attachment of the Portal.
|
||||
flushMicrotasks();
|
||||
|
||||
// Expect that the content of the attached portal is present.
|
||||
let hostContainer = appFixture.nativeElement.querySelector('.portal-container');
|
||||
expect(hostContainer.textContent).toContain('Cake');
|
||||
}));
|
||||
|
||||
it('should load a <template> portal with the `*` sugar', fakeAsyncTest(() => {
|
||||
let appFixture: ComponentFixture;
|
||||
|
||||
builder.createAsync(PortalTestApp).then(fixture => {
|
||||
appFixture = fixture;
|
||||
});
|
||||
|
||||
// Flush the async creation of the PortalTestApp.
|
||||
flushMicrotasks();
|
||||
|
||||
let testAppComponent = appFixture.debugElement.componentInstance;
|
||||
|
||||
// Detect changes initially so that the component's ViewChildren are resolved.
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Set the selectedHost to be a TemplatePortal (with the `*` syntax).
|
||||
testAppComponent.selectedPortal = testAppComponent.piePortal;
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Flush the attachment of the Portal.
|
||||
flushMicrotasks();
|
||||
|
||||
// Expect that the content of the attached portal is present.
|
||||
let hostContainer = appFixture.nativeElement.querySelector('.portal-container');
|
||||
expect(hostContainer.textContent).toContain('Pie');
|
||||
}));
|
||||
|
||||
it('should load a <template> portal with a binding', fakeAsyncTest(() => {
|
||||
let appFixture: ComponentFixture;
|
||||
|
||||
builder.createAsync(PortalTestApp).then(fixture => {
|
||||
appFixture = fixture;
|
||||
});
|
||||
|
||||
// Flush the async creation of the PortalTestApp.
|
||||
flushMicrotasks();
|
||||
|
||||
let testAppComponent = appFixture.debugElement.componentInstance;
|
||||
|
||||
// Detect changes initially so that the component's ViewChildren are resolved.
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Set the selectedHost to be a TemplatePortal.
|
||||
testAppComponent.selectedPortal = testAppComponent.portalWithBinding;
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Flush the attachment of the Portal.
|
||||
flushMicrotasks();
|
||||
|
||||
// Now that the portal is attached, change detection has to happen again in order
|
||||
// for the bindings to update.
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Expect that the content of the attached portal is present.
|
||||
let hostContainer = appFixture.nativeElement.querySelector('.portal-container');
|
||||
expect(hostContainer.textContent).toContain('Banana');
|
||||
}));
|
||||
|
||||
it('should load a <template> portal with extra locals', fakeAsyncTest(() => {
|
||||
let appFixture: ComponentFixture;
|
||||
|
||||
builder.createAsync(PortalTestApp).then(fixture => {
|
||||
appFixture = fixture;
|
||||
});
|
||||
|
||||
// Flush the async creation of the PortalTestApp.
|
||||
flushMicrotasks();
|
||||
|
||||
let testAppComponent = appFixture.debugElement.componentInstance;
|
||||
|
||||
// Detect changes initially so that the component's ViewChildren are resolved.
|
||||
appFixture.detectChanges();
|
||||
|
||||
let locals = new Map<string, string>();
|
||||
locals.set('appetizer', 'Samosa');
|
||||
|
||||
let templatePortal = testAppComponent.portalWithLocals;
|
||||
templatePortal.locals = locals;
|
||||
|
||||
// Set the selectedHost to be a TemplatePortal.
|
||||
testAppComponent.selectedPortal = templatePortal;
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Flush the attachment of the Portal.
|
||||
flushMicrotasks();
|
||||
|
||||
// Now that the portal is attached, change detection has to happen again in order
|
||||
// for the bindings to update.
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Expect that the content of the attached portal is present.
|
||||
let hostContainer = appFixture.nativeElement.querySelector('.portal-container');
|
||||
expect(hostContainer.textContent).toContain('Samosa');
|
||||
}));
|
||||
|
||||
it('should change the attached portal', fakeAsyncTest(() => {
|
||||
let appFixture: ComponentFixture;
|
||||
|
||||
builder.createAsync(PortalTestApp).then(fixture => {
|
||||
appFixture = fixture;
|
||||
});
|
||||
|
||||
// Flush the async creation of the PortalTestApp.
|
||||
flushMicrotasks();
|
||||
|
||||
let testAppComponent = appFixture.debugElement.componentInstance;
|
||||
|
||||
// Detect changes initially so that the component's ViewChildren are resolved.
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Set the selectedHost to be a ComponentPortal.
|
||||
testAppComponent.selectedPortal = testAppComponent.piePortal;
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Flush the attachment of the Portal.
|
||||
flushMicrotasks();
|
||||
appFixture.detectChanges();
|
||||
|
||||
// Expect that the content of the attached portal is present.
|
||||
let hostContainer = appFixture.nativeElement.querySelector('.portal-container');
|
||||
expect(hostContainer.textContent).toContain('Pie');
|
||||
|
||||
testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg);
|
||||
appFixture.detectChanges();
|
||||
|
||||
flushMicrotasks();
|
||||
|
||||
expect(hostContainer.textContent).toContain('Pizza');
|
||||
}));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/** Simple component for testing ComponentPortal. */
|
||||
@Component({
|
||||
selector: 'pizza-msg',
|
||||
template: '<p>Pizza</p>',
|
||||
})
|
||||
class PizzaMsg {}
|
||||
|
||||
|
||||
/** Test-bed component that contains a portal host and a couple of template portals. */
|
||||
@Component({
|
||||
selector: 'portal-test',
|
||||
template: `
|
||||
<div class="portal-container">
|
||||
<template [portalHost]="selectedPortal"></template>
|
||||
</div>
|
||||
|
||||
<template portal>Cake</template>
|
||||
|
||||
<div *portal>Pie</div>
|
||||
|
||||
<template portal> {{fruit}} </template>
|
||||
|
||||
<template portal #yum="appetizer">{{yum}}</template>
|
||||
`,
|
||||
directives: [PortalHostDirective, TemplatePortalDirective],
|
||||
})
|
||||
class PortalTestApp {
|
||||
@ViewChildren(TemplatePortalDirective) portals: QueryList<TemplatePortalDirective>;
|
||||
selectedPortal: Portal<any>;
|
||||
fruit: string = 'Banana';
|
||||
|
||||
get cakePortal() {
|
||||
return this.portals.first;
|
||||
}
|
||||
|
||||
get piePortal() {
|
||||
return this.portals.toArray()[1];
|
||||
}
|
||||
|
||||
get portalWithBinding() {
|
||||
return this.portals.toArray()[2];
|
||||
}
|
||||
|
||||
get portalWithLocals() {
|
||||
return this.portals.last;
|
||||
}
|
||||
}
|
||||
|
||||
function fakeAsyncTest(fn: () => void) {
|
||||
return inject([], fakeAsync(fn));
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import {TemplateRef, Type} from 'angular2/core';
|
||||
import {ElementRef} from 'angular2/core';
|
||||
import {ComponentRef} from 'angular2/core';
|
||||
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
|
||||
|
||||
/**
|
||||
* A `Portal` is something that you want to render somewhere else.
|
||||
* It can be attach to / detached from a `PortalHost`.
|
||||
*/
|
||||
export abstract class Portal<T> {
|
||||
private attachedHost_: PortalHost;
|
||||
|
||||
/** Attach this portal to a host. */
|
||||
attach(host: PortalHost): Promise<T> {
|
||||
if (host == null) {
|
||||
throw new BaseException('Attempting to attach a portal to a null host');
|
||||
}
|
||||
|
||||
if (host.hasAttached()) {
|
||||
throw new BaseException('Host already has a portal attached');
|
||||
}
|
||||
|
||||
this.attachedHost_ = host;
|
||||
return <Promise<T>> host.attach(this);
|
||||
}
|
||||
|
||||
/** Detach this portal from its host */
|
||||
detach(): Promise<void> {
|
||||
let host = this.attachedHost_;
|
||||
if (host == null) {
|
||||
throw new BaseException('Portal has no host from which to detach');
|
||||
}
|
||||
|
||||
this.attachedHost_ = null;
|
||||
return host.detach();
|
||||
}
|
||||
|
||||
/** Whether this portal is attached to a host. */
|
||||
get isAttached(): boolean {
|
||||
return this.attachedHost_ != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PortalHost reference without performing `attach()`. This is used directly by
|
||||
* the PortalHost when it is performing an `attach()` or `detatch()`.
|
||||
*/
|
||||
setAttachedHost(host: PortalHost) {
|
||||
this.attachedHost_ = host;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A `ComponentPortal` is a portal that instantiates some Component upon attachment.
|
||||
*/
|
||||
export class ComponentPortal extends Portal<ComponentRef> {
|
||||
/** The type of the component that will be instantiated for attachment. */
|
||||
public component: Type;
|
||||
|
||||
/**
|
||||
* [Optional] Where the attached component should live in Angular's *logical* component tree.
|
||||
* This is different from where the component *renders*, which is determined by the PortalHost.
|
||||
* The origin necessary when the host is outside of the Angular application context.
|
||||
*/
|
||||
public origin: ElementRef;
|
||||
|
||||
constructor(component: Type, origin: ElementRef = null) {
|
||||
super();
|
||||
this.component = component;
|
||||
this.origin = origin;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A `TemplatePortal` is a portal that represents some embedded template (TemplateRef).
|
||||
*/
|
||||
export class TemplatePortal extends Portal<Map<string, any>> {
|
||||
/** The embedded template that will be used to instantiate an embedded View in the host. */
|
||||
templateRef: TemplateRef;
|
||||
|
||||
/**
|
||||
* Additional locals for the instantiated embedded view.
|
||||
* These locals can be seen as "exports" for the template, such as how ngFor has
|
||||
* index / event / odd.
|
||||
* See https://angular.io/docs/ts/latest/api/core/EmbeddedViewRef-class.html
|
||||
*/
|
||||
locals: Map<string, any> = new Map<string, any>();
|
||||
|
||||
constructor(template: TemplateRef) {
|
||||
super();
|
||||
this.templateRef = template;
|
||||
}
|
||||
|
||||
get origin(): ElementRef {
|
||||
return this.templateRef.elementRef;
|
||||
}
|
||||
|
||||
attach(host: PortalHost, locals?: Map<string, any>): Promise<Map<string, any>> {
|
||||
this.locals = locals == null ? new Map<string, any>() : locals;
|
||||
return super.attach(host);
|
||||
}
|
||||
|
||||
detach(): Promise<void> {
|
||||
this.locals = new Map<string, any>();
|
||||
return super.detach();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A `PortalHost` is an space that can contain a single `Portal`.
|
||||
*/
|
||||
export interface PortalHost {
|
||||
attach(portal: Portal<any>): Promise<any>;
|
||||
|
||||
detach(): Promise<any>;
|
||||
|
||||
dispose(): void;
|
||||
|
||||
hasAttached(): boolean;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Partial implementation of PortalHost that only deals with attaching either a
|
||||
* ComponentPortal or a TemplatePortal.
|
||||
*/
|
||||
export abstract class BasePortalHost implements PortalHost {
|
||||
/** The portal currently attached to the host. */
|
||||
private attachedPortal_: Portal<any>;
|
||||
|
||||
/** A function that will permanently dispose this host. */
|
||||
private disposeFn_: () => void;
|
||||
|
||||
/** Whether this host has already been permanently disposed. */
|
||||
private isDisposed_: boolean = false;
|
||||
|
||||
/** Whether this host has an attached portal. */
|
||||
hasAttached() {
|
||||
return this.attachedPortal_ != null;
|
||||
}
|
||||
|
||||
attach(portal: Portal<any>): Promise<any> {
|
||||
if (portal == null) {
|
||||
throw new BaseException('Must provide a portal to attach');
|
||||
}
|
||||
|
||||
if (this.hasAttached()) {
|
||||
throw new BaseException('A portal is already attached');
|
||||
}
|
||||
|
||||
if (this.isDisposed_) {
|
||||
throw new BaseException('This PortalHost has already been disposed');
|
||||
}
|
||||
|
||||
if (portal instanceof ComponentPortal) {
|
||||
this.attachedPortal_ = portal;
|
||||
return this.attachComponentPortal(portal);
|
||||
} else if (portal instanceof TemplatePortal) {
|
||||
this.attachedPortal_ = portal;
|
||||
return this.attachTemplatePortal(portal);
|
||||
}
|
||||
|
||||
throw new BaseException(
|
||||
'Attempting to attach an unknown Portal type. ' +
|
||||
'BasePortalHost accepts either a ComponentPortal or a TemplatePortal.');
|
||||
}
|
||||
|
||||
abstract attachComponentPortal(portal: ComponentPortal): Promise<ComponentRef>;
|
||||
|
||||
abstract attachTemplatePortal(portal: TemplatePortal): Promise<Map<string, any>>;
|
||||
|
||||
detach(): Promise<void> {
|
||||
this.attachedPortal_.setAttachedHost(null);
|
||||
this.attachedPortal_ = null;
|
||||
if (this.disposeFn_ != null) {
|
||||
this.disposeFn_();
|
||||
this.disposeFn_ = null;
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.hasAttached()) {
|
||||
this.detach();
|
||||
}
|
||||
|
||||
this.isDisposed_ = true;
|
||||
}
|
||||
|
||||
setDisposeFn(fn: () => void) {
|
||||
this.disposeFn_ = fn;
|
||||
}
|
||||
}
|
||||
+16
-14
@@ -1,14 +1,16 @@
|
||||
|
||||
<h1 class="title">Angular Material2 Demos</h1>
|
||||
<ul>
|
||||
<li><a [routerLink]="['ButtonDemo']">Button demo</a></li>
|
||||
<li><a [routerLink]="['CardDemo']">Card demo</a></li>
|
||||
<li><a [routerLink]="['SidenavDemo']">Sidenav demo</a></li>
|
||||
<li><a [routerLink]="['ProgressCircleDemo']">Progress Circle demo</a></li>
|
||||
</ul>
|
||||
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
|
||||
{{root.dir.toUpperCase()}}
|
||||
</button>
|
||||
<div #root="$implicit" dir="ltr">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<h1 class="title">Angular Material2 Demos</h1>
|
||||
<ul>
|
||||
<li><a [routerLink]="['ButtonDemo']">Button demo</a></li>
|
||||
<li><a [routerLink]="['CardDemo']">Card demo</a></li>
|
||||
<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>
|
||||
</ul>
|
||||
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
|
||||
{{root.dir.toUpperCase()}}
|
||||
</button>
|
||||
<div #root="$implicit" dir="ltr">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
div.content {
|
||||
padding: 10px;
|
||||
.demo-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import {ProgressCircleDemo} from './progress-circle/progress-circle-demo';
|
||||
import {Route, RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
|
||||
import {Dir} from '../directives/dir/dir';
|
||||
import {MdButton} from '../components/button/button';
|
||||
import {PortalDemo} from './portal/portal-demo';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'home',
|
||||
@@ -27,5 +29,6 @@ export class Home {}
|
||||
new Route({path: '/card', name: 'CardDemo', component: CardDemo}),
|
||||
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}),
|
||||
])
|
||||
export class DemoApp { }
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<h2> The portal host is here: </h2>
|
||||
<div class="demo-portal-host">
|
||||
<template [portalHost]="selectedPortal"></template>
|
||||
</div>
|
||||
|
||||
<button type="button" (click)="selectedPortal = programmingJoke">
|
||||
Programming joke
|
||||
</button>
|
||||
|
||||
<button type="button" (click)="selectedPortal = mathJoke">
|
||||
Math joke
|
||||
</button>
|
||||
|
||||
<button type="button" (click)="selectedPortal = scienceJoke">
|
||||
Science joke
|
||||
</button>
|
||||
|
||||
<!-- Template vars on <template> elements can't be accessed _in_ the template because Angular
|
||||
doesn't support grabbing the instance / TemplateRef this way because the variable may be
|
||||
referring to something *in* the template (such as #item in ngFor). As such, the component
|
||||
has to use @ViewChild / @ViewChildren to get these references.
|
||||
See https://github.com/angular/angular/issues/7158 -->
|
||||
<template portal>
|
||||
<p> - Why don't jokes work in octal?</p>
|
||||
<p> - Because 7 10 11.</p>
|
||||
</template>
|
||||
|
||||
<div *portal>
|
||||
<p> - Did you hear about this year's Fibonacci Conference? </p>
|
||||
<p> - It's going to be as the last two put together. </p>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
.demo-portal-host {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border: 1px dashed black;
|
||||
width: 500px;
|
||||
height: 100px;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {Component} from 'angular2/core';
|
||||
import {PortalHostDirective} from '../../core/portal/portal-directives';
|
||||
import {TemplatePortalDirective} from '../../core/portal/portal-directives';
|
||||
import {Portal} from '../../core/portal/portal';
|
||||
import {ViewChildren} from 'angular2/core';
|
||||
import {QueryList} from 'angular2/core';
|
||||
import {ComponentPortal} from '../../core/portal/portal';
|
||||
|
||||
@Component({
|
||||
selector: 'portal-demo',
|
||||
templateUrl: 'demo-app/portal/portal-demo.html',
|
||||
styleUrls: ['demo-app/portal/portal-demo.css'],
|
||||
directives: [TemplatePortalDirective, PortalHostDirective]
|
||||
})
|
||||
export class PortalDemo {
|
||||
@ViewChildren(TemplatePortalDirective) templatePortals: QueryList<Portal<any>>;
|
||||
|
||||
selectedPortal: Portal<any>;
|
||||
|
||||
constructor() {
|
||||
console.log('~~ contructor ~~');
|
||||
}
|
||||
|
||||
get programmingJoke() {
|
||||
return this.templatePortals.first;
|
||||
}
|
||||
|
||||
get mathJoke() {
|
||||
return this.templatePortals.last;
|
||||
}
|
||||
|
||||
get scienceJoke() {
|
||||
return new ComponentPortal(ScienceJoke);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'science-joke',
|
||||
template: `<p> 100 kilopascals go into a bar. </p>`
|
||||
})
|
||||
class ScienceJoke { }
|
||||
+1
-1
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Material2</title>
|
||||
<base href="/">
|
||||
<script src="vendor/systemjs/dist/system-polyfills.js"></script>
|
||||
<script src="vendor/angular2/bundles/angular2-polyfills.js"></script>
|
||||
{{content-for 'head'}}
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
@@ -14,7 +15,6 @@
|
||||
<demo-app>Loading...</demo-app>
|
||||
|
||||
<script src="vendor/systemjs/dist/system.src.js"></script>
|
||||
<script src="vendor/systemjs/dist/system-polyfills.js"></script>
|
||||
<script src="vendor/rxjs/bundles/Rx.js"></script>
|
||||
<script src="vendor/angular2/bundles/angular2.dev.js"></script>
|
||||
<script src="vendor/angular2/bundles/http.dev.js"></script>
|
||||
|
||||
+6
-1
@@ -1,5 +1,10 @@
|
||||
import {bootstrap} from 'angular2/platform/browser';
|
||||
import {DemoApp} from './demo-app/demo-app';
|
||||
import {ROUTER_PROVIDERS} from 'angular2/router';
|
||||
import {HashLocationStrategy} from 'angular2/router';
|
||||
import {LocationStrategy} from 'angular2/router';
|
||||
import {provide} from 'angular2/core';
|
||||
|
||||
bootstrap(DemoApp, [ROUTER_PROVIDERS]);
|
||||
bootstrap(DemoApp, [
|
||||
ROUTER_PROVIDERS
|
||||
]);
|
||||
|
||||
@@ -23,6 +23,8 @@ export function config(config) {
|
||||
{pattern: 'node_modules/systemjs/dist/system-polyfills.js', included: true, watched: true},
|
||||
'node_modules/angular2/es6/dev/src/testing/shims_for_IE.js',
|
||||
|
||||
// Angular 2 polyfills *must* be loaded after es6-shim and system-polyfills in order to
|
||||
// setup the monkey-patches for zones.
|
||||
{pattern: 'node_modules/angular2/bundles/angular2-polyfills.js', included: true, watched: true},
|
||||
{pattern: 'node_modules/systemjs/dist/system.src.js', included: true, watched: true},
|
||||
{pattern: 'node_modules/rxjs/bundles/Rx.js', included: true, watched: true},
|
||||
|
||||
Reference in New Issue
Block a user