mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(portal): support context in TemplatePortal (#6408)
BREAKING CHANGE: - `TemplatePortal` now requires a generic type (C) - method `attach` return type C and property `locals` was removed - method `attachTemplatePortal` in `BasePortalHost` now requires a generic type (C) and returns EmbeddedViewRef<C> (also applies to extending types `DomPortalHost`, `PortalHostDirective` and `MdDialogContainer`)
This commit is contained in:
@@ -93,7 +93,7 @@ export class OverlayOrigin {
|
||||
})
|
||||
export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
|
||||
private _overlayRef: OverlayRef;
|
||||
private _templatePortal: TemplatePortal;
|
||||
private _templatePortal: TemplatePortal<any>;
|
||||
private _hasBackdrop = false;
|
||||
private _backdropSubscription: Subscription | null;
|
||||
private _positionSubscription: Subscription;
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
describe('Overlay', () => {
|
||||
let overlay: Overlay;
|
||||
let componentPortal: ComponentPortal<PizzaMsg>;
|
||||
let templatePortal: TemplatePortal;
|
||||
let templatePortal: TemplatePortal<any>;
|
||||
let overlayContainerElement: HTMLElement;
|
||||
let viewContainerFixture: ComponentFixture<TestComponentWithTemplatePortals>;
|
||||
|
||||
|
||||
@@ -69,9 +69,9 @@ export class DomPortalHost extends BasePortalHost {
|
||||
* Attaches a template portal to the DOM as an embedded view.
|
||||
* @param portal Portal to be attached.
|
||||
*/
|
||||
attachTemplatePortal(portal: TemplatePortal): Map<string, any> {
|
||||
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
|
||||
let viewContainer = portal.viewContainerRef;
|
||||
let viewRef = viewContainer.createEmbeddedView(portal.templateRef);
|
||||
let viewRef = viewContainer.createEmbeddedView(portal.templateRef, portal.context);
|
||||
viewRef.detectChanges();
|
||||
|
||||
// The method `createEmbeddedView` will add the view as a child of the viewContainer.
|
||||
@@ -87,7 +87,7 @@ export class DomPortalHost extends BasePortalHost {
|
||||
}));
|
||||
|
||||
// TODO(jelbourn): Return locals from view.
|
||||
return new Map<string, any>();
|
||||
return viewRef;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
NgModule,
|
||||
ComponentRef,
|
||||
Directive,
|
||||
EmbeddedViewRef,
|
||||
TemplateRef,
|
||||
ComponentFactoryResolver,
|
||||
ViewContainerRef,
|
||||
@@ -32,7 +33,7 @@ import {Portal, TemplatePortal, ComponentPortal, BasePortalHost} from './portal'
|
||||
selector: '[cdk-portal], [cdkPortal], [portal]',
|
||||
exportAs: 'cdkPortal',
|
||||
})
|
||||
export class TemplatePortalDirective extends TemplatePortal {
|
||||
export class TemplatePortalDirective extends TemplatePortal<any> {
|
||||
constructor(templateRef: TemplateRef<any>, viewContainerRef: ViewContainerRef) {
|
||||
super(templateRef, viewContainerRef);
|
||||
}
|
||||
@@ -117,16 +118,14 @@ export class PortalHostDirective extends BasePortalHost implements OnDestroy {
|
||||
* Attach the given TemplatePortal to this PortlHost as an embedded View.
|
||||
* @param portal Portal to be attached.
|
||||
*/
|
||||
attachTemplatePortal(portal: TemplatePortal): Map<string, any> {
|
||||
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
|
||||
portal.setAttachedHost(this);
|
||||
|
||||
this._viewContainerRef.createEmbeddedView(portal.templateRef);
|
||||
const viewRef = this._viewContainerRef.createEmbeddedView(portal.templateRef, portal.context);
|
||||
super.setDisposeFn(() => this._viewContainerRef.clear());
|
||||
|
||||
this._portal = portal;
|
||||
|
||||
// TODO(jelbourn): return locals from view
|
||||
return new Map<string, any>();
|
||||
return viewRef;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
Optional,
|
||||
Injector,
|
||||
ApplicationRef,
|
||||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {TemplatePortalDirective, PortalHostDirective, PortalModule} from './portal-directives';
|
||||
import {Portal, ComponentPortal} from './portal';
|
||||
import {Portal, ComponentPortal, TemplatePortal} from './portal';
|
||||
import {DomPortalHost} from './dom-portal-host';
|
||||
|
||||
|
||||
@@ -45,6 +46,57 @@ describe('Portals', () => {
|
||||
expect(hostContainer.textContent).toContain('Pizza');
|
||||
});
|
||||
|
||||
it('should load a template into the portal', () => {
|
||||
let testAppComponent = fixture.debugElement.componentInstance;
|
||||
let hostContainer = fixture.nativeElement.querySelector('.portal-container');
|
||||
|
||||
let templatePortal = new TemplatePortal(testAppComponent.templateRef, null!);
|
||||
testAppComponent.selectedPortal = templatePortal;
|
||||
fixture.detectChanges();
|
||||
// Expect that the content of the attached portal is present and no context is projected
|
||||
expect(hostContainer.textContent).toContain('Banana');
|
||||
});
|
||||
|
||||
it('should project template context bindings in the portal', () => {
|
||||
let testAppComponent = fixture.debugElement.componentInstance;
|
||||
let hostContainer = fixture.nativeElement.querySelector('.portal-container');
|
||||
|
||||
// TemplatePortal without context:
|
||||
let templatePortal = new TemplatePortal(testAppComponent.templateRef, null!);
|
||||
testAppComponent.selectedPortal = templatePortal;
|
||||
fixture.detectChanges();
|
||||
// Expect that the content of the attached portal is present and NO context is projected
|
||||
expect(hostContainer.textContent).toContain('Banana - !');
|
||||
|
||||
// using TemplatePortal.attach method to set context
|
||||
testAppComponent.selectedPortal = undefined;
|
||||
fixture.detectChanges();
|
||||
templatePortal.attach(testAppComponent.portalHost, {$implicit: {status: 'rotten'}});
|
||||
fixture.detectChanges();
|
||||
// Expect that the content of the attached portal is present and context given via the
|
||||
// attach method is projected
|
||||
expect(hostContainer.textContent).toContain('Banana - rotten!');
|
||||
|
||||
// using TemplatePortal constructor to set the context
|
||||
templatePortal =
|
||||
new TemplatePortal(testAppComponent.templateRef, null!, {$implicit: {status: 'fresh'}});
|
||||
testAppComponent.selectedPortal = templatePortal;
|
||||
fixture.detectChanges();
|
||||
// Expect that the content of the attached portal is present and context given via the
|
||||
// constructor is projected
|
||||
expect(hostContainer.textContent).toContain('Banana - fresh!');
|
||||
|
||||
// using TemplatePortal constructor to set the context but also calling attach method with
|
||||
// context, the latter should take precedence:
|
||||
testAppComponent.selectedPortal = undefined;
|
||||
fixture.detectChanges();
|
||||
templatePortal.attach(testAppComponent.portalHost, {$implicit: {status: 'rotten'}});
|
||||
fixture.detectChanges();
|
||||
// Expect that the content of the attached portal is present and and context given via the
|
||||
// attach method is projected and get precedence over constructor context
|
||||
expect(hostContainer.textContent).toContain('Banana - rotten!');
|
||||
});
|
||||
|
||||
it('should dispose the host when destroyed', () => {
|
||||
// Set the selectedHost to be a ComponentPortal.
|
||||
let testAppComponent = fixture.debugElement.componentInstance;
|
||||
@@ -299,7 +351,7 @@ describe('Portals', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// Attach the TemplatePortal.
|
||||
testAppComponent.portalWithBinding.attach(host);
|
||||
testAppComponent.portalWithBinding.attach(host, {$implicit: {status: 'fresh'}});
|
||||
fixture.detectChanges();
|
||||
|
||||
// Now that the portal is attached, change detection has to happen again in order
|
||||
@@ -307,7 +359,7 @@ describe('Portals', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// Expect that the content of the attached portal is present.
|
||||
expect(someDomElement.textContent).toContain('Banana');
|
||||
expect(someDomElement.textContent).toContain('Banana - fresh');
|
||||
|
||||
// When updating the binding value.
|
||||
testAppComponent.fruit = 'Mango';
|
||||
@@ -416,18 +468,22 @@ class ArbitraryViewContainerRefComponent {
|
||||
<ng-template cdk-portal>Cake</ng-template>
|
||||
|
||||
<div *cdk-portal>Pie</div>
|
||||
<ng-template cdk-portal> {{fruit}} </ng-template>
|
||||
<ng-template cdk-portal let-data> {{fruit}} - {{ data?.status }} </ng-template>
|
||||
|
||||
<ng-template cdk-portal>
|
||||
<ul>
|
||||
<li *ngFor="let fruitName of fruits"> {{fruitName}} </li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #templateRef let-data> {{fruit}} - {{ data?.status }}!</ng-template>
|
||||
`,
|
||||
})
|
||||
class PortalTestApp {
|
||||
@ViewChildren(TemplatePortalDirective) portals: QueryList<TemplatePortalDirective>;
|
||||
@ViewChild(PortalHostDirective) portalHost: PortalHostDirective;
|
||||
@ViewChild('templateRef', { read: TemplateRef }) templateRef: TemplateRef<any>;
|
||||
|
||||
selectedPortal: Portal<any>;
|
||||
fruit: string = 'Banana';
|
||||
fruits = ['Apple', 'Pineapple', 'Durian'];
|
||||
@@ -449,6 +505,7 @@ class PortalTestApp {
|
||||
get portalWithTemplate() {
|
||||
return this.portals.toArray()[3];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create a real (non-test) NgModule as a workaround for
|
||||
|
||||
+17
-15
@@ -11,6 +11,7 @@ import {
|
||||
ViewContainerRef,
|
||||
ElementRef,
|
||||
ComponentRef,
|
||||
EmbeddedViewRef,
|
||||
Injector
|
||||
} from '@angular/core';
|
||||
import {
|
||||
@@ -103,42 +104,43 @@ export class ComponentPortal<T> extends Portal<ComponentRef<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A `TemplatePortal` is a portal that represents some embedded template (TemplateRef).
|
||||
*/
|
||||
export class TemplatePortal extends Portal<Map<string, any>> {
|
||||
export class TemplatePortal<C> extends Portal<C> {
|
||||
/** The embedded template that will be used to instantiate an embedded View in the host. */
|
||||
templateRef: TemplateRef<any>;
|
||||
templateRef: TemplateRef<C>;
|
||||
|
||||
/** Reference to the ViewContainer into which the template will be stamped out. */
|
||||
viewContainerRef: ViewContainerRef;
|
||||
|
||||
/**
|
||||
* 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>();
|
||||
context: C | undefined;
|
||||
|
||||
constructor(template: TemplateRef<any>, viewContainerRef: ViewContainerRef) {
|
||||
constructor(template: TemplateRef<any>, viewContainerRef: ViewContainerRef, context?: C) {
|
||||
super();
|
||||
this.templateRef = template;
|
||||
this.viewContainerRef = viewContainerRef;
|
||||
if (context) {
|
||||
this.context = context;
|
||||
}
|
||||
}
|
||||
|
||||
get origin(): ElementRef {
|
||||
return this.templateRef.elementRef;
|
||||
}
|
||||
|
||||
attach(host: PortalHost, locals?: Map<string, any>): Map<string, any> {
|
||||
this.locals = locals == null ? new Map<string, any>() : locals;
|
||||
/**
|
||||
* Attach the the portal to the provided `PortalHost`.
|
||||
* When a context is provided it will override the `context` property of the `TemplatePortal`
|
||||
* instance.
|
||||
*/
|
||||
attach(host: PortalHost, context: C | undefined = this.context): C {
|
||||
this.context = context;
|
||||
return super.attach(host);
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
this.locals = new Map<string, any>();
|
||||
this.context = undefined;
|
||||
return super.detach();
|
||||
}
|
||||
}
|
||||
@@ -203,7 +205,7 @@ export abstract class BasePortalHost implements PortalHost {
|
||||
|
||||
abstract attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;
|
||||
|
||||
abstract attachTemplatePortal(portal: TemplatePortal): Map<string, any>;
|
||||
abstract attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;
|
||||
|
||||
detach(): void {
|
||||
if (this._attachedPortal) {
|
||||
|
||||
@@ -78,6 +78,16 @@
|
||||
<p>Last afterClosed result: {{lastAfterClosedResult}}</p>
|
||||
<p>Last beforeClose result: {{lastBeforeCloseResult}}</p>
|
||||
|
||||
<ng-template>
|
||||
<ng-template let-data let-dialogRef="dialogRef">
|
||||
I'm a template dialog. I've been opened {{numTemplateOpens}} times!
|
||||
|
||||
<p>It's Jazz!</p>
|
||||
|
||||
<md-input-container>
|
||||
<input mdInput placeholder="How much?" #howMuch>
|
||||
</md-input-container>
|
||||
|
||||
<p> {{ data.message }} </p>
|
||||
<button type="button" (click)="dialogRef.close(lastCloseResult = howMuch.value)">Close dialog</button>
|
||||
<button (click)="dialogRef.updateSize('500px', '500px').updatePosition({ top: '25px', left: '25px' });">Change dimensions</button>`
|
||||
</ng-template>
|
||||
|
||||
@@ -118,7 +118,7 @@ export function getMdAutocompleteMissingPanelError(): Error {
|
||||
})
|
||||
export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
|
||||
private _overlayRef: OverlayRef | null;
|
||||
private _portal: TemplatePortal;
|
||||
private _portal: TemplatePortal<any>;
|
||||
private _panelOpen: boolean = false;
|
||||
|
||||
/** Strategy that is used to position the panel. */
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Component,
|
||||
ComponentRef,
|
||||
ElementRef,
|
||||
EmbeddedViewRef,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
NgZone,
|
||||
@@ -124,7 +125,7 @@ export class MdDialogContainer extends BasePortalHost {
|
||||
* Attach a TemplatePortal as content to this dialog container.
|
||||
* @param portal Portal to be attached as the dialog content.
|
||||
*/
|
||||
attachTemplatePortal(portal: TemplatePortal): Map<string, any> {
|
||||
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
|
||||
if (this._portalHost.hasAttached()) {
|
||||
throwMdDialogContentAlreadyAttachedError();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Injector,
|
||||
Inject,
|
||||
ChangeDetectionStrategy,
|
||||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
@@ -79,6 +80,28 @@ describe('MdDialog', () => {
|
||||
expect(dialogContainerElement.getAttribute('role')).toBe('dialog');
|
||||
});
|
||||
|
||||
it('should open a dialog with a template', () => {
|
||||
const templateRefFixture = TestBed.createComponent(ComponentWithTemplateRef);
|
||||
templateRefFixture.componentInstance.localValue = 'Bees';
|
||||
templateRefFixture.detectChanges();
|
||||
|
||||
const data = {value: 'Knees'};
|
||||
|
||||
let dialogRef = dialog.open(templateRefFixture.componentInstance.templateRef, { data });
|
||||
|
||||
viewContainerFixture.detectChanges();
|
||||
|
||||
expect(overlayContainerElement.textContent).toContain('Cheese Bees Knees');
|
||||
expect(templateRefFixture.componentInstance.dialogRef).toBe(dialogRef);
|
||||
|
||||
viewContainerFixture.detectChanges();
|
||||
|
||||
let dialogContainerElement = overlayContainerElement.querySelector('md-dialog-container')!;
|
||||
expect(dialogContainerElement.getAttribute('role')).toBe('dialog');
|
||||
|
||||
dialogRef.close();
|
||||
});
|
||||
|
||||
it('should use injector from viewContainerRef for DialogInjector', () => {
|
||||
let dialogRef = dialog.open(PizzaMsg, {
|
||||
viewContainerRef: testViewContainerRef
|
||||
@@ -876,6 +899,23 @@ class ComponentWithChildViewContainer {
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'arbitrary-component-with-template-ref',
|
||||
template: `<ng-template let-data let-dialogRef="dialogRef">
|
||||
Cheese {{localValue}} {{data?.value}}{{setDialogRef(dialogRef)}}</ng-template>`,
|
||||
})
|
||||
class ComponentWithTemplateRef {
|
||||
localValue: string;
|
||||
dialogRef: MdDialogRef<any>;
|
||||
|
||||
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||
|
||||
setDialogRef(dialogRef: MdDialogRef<any>): string {
|
||||
this.dialogRef = dialogRef;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Simple component for testing ComponentPortal. */
|
||||
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
|
||||
class PizzaMsg {
|
||||
@@ -916,6 +956,7 @@ class DialogWithInjectedData {
|
||||
// https://github.com/angular/angular/issues/10760
|
||||
const TEST_DIRECTIVES = [
|
||||
ComponentWithChildViewContainer,
|
||||
ComponentWithTemplateRef,
|
||||
PizzaMsg,
|
||||
DirectiveWithViewContainer,
|
||||
ComponentWithOnPushViewContainer,
|
||||
@@ -929,6 +970,7 @@ const TEST_DIRECTIVES = [
|
||||
declarations: TEST_DIRECTIVES,
|
||||
entryComponents: [
|
||||
ComponentWithChildViewContainer,
|
||||
ComponentWithTemplateRef,
|
||||
PizzaMsg,
|
||||
ContentElementDialog,
|
||||
DialogWithInjectedData
|
||||
|
||||
@@ -242,7 +242,9 @@ export class MdDialog {
|
||||
}
|
||||
|
||||
if (componentOrTemplateRef instanceof TemplateRef) {
|
||||
dialogContainer.attachTemplatePortal(new TemplatePortal(componentOrTemplateRef, null!));
|
||||
dialogContainer.attachTemplatePortal(
|
||||
new TemplatePortal<T>(componentOrTemplateRef, null!,
|
||||
<any>{ $implicit: config.data, dialogRef }));
|
||||
} else {
|
||||
const injector = this._createInjector<T>(config, dialogRef, dialogContainer);
|
||||
const contentRef = dialogContainer.attachComponentPortal(
|
||||
|
||||
@@ -86,7 +86,7 @@ export const MENU_PANEL_TOP_PADDING = 8;
|
||||
exportAs: 'mdMenuTrigger'
|
||||
})
|
||||
export class MdMenuTrigger implements AfterViewInit, OnDestroy {
|
||||
private _portal: TemplatePortal;
|
||||
private _portal: TemplatePortal<any>;
|
||||
private _overlayRef: OverlayRef | null = null;
|
||||
private _menuOpen: boolean = false;
|
||||
private _closeSubscription: Subscription;
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentRef,
|
||||
EmbeddedViewRef,
|
||||
ViewChild,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
Renderer2,
|
||||
ElementRef,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectionStrategy
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
@@ -109,7 +110,7 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
|
||||
}
|
||||
|
||||
/** Attach a template portal as content to this snack bar container. */
|
||||
attachTemplatePortal(): Map<string, any> {
|
||||
attachTemplatePortal(): EmbeddedViewRef<any> {
|
||||
throw Error('Not yet implemented');
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ describe('MdTabBody', () => {
|
||||
`
|
||||
})
|
||||
class SimpleTabBodyApp {
|
||||
content: TemplatePortal;
|
||||
content: TemplatePortal<any>;
|
||||
position: number;
|
||||
origin: number;
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ export class MdTabBody implements OnInit, AfterViewChecked {
|
||||
@Output() onCentered: EventEmitter<void> = new EventEmitter<void>(true);
|
||||
|
||||
/** The tab body content to display. */
|
||||
@Input('content') _content: TemplatePortal;
|
||||
@Input('content') _content: TemplatePortal<any>;
|
||||
|
||||
/** The shifted index position of the tab body, where zero represents the active center tab. */
|
||||
_position: MdTabBodyPositionState;
|
||||
|
||||
+2
-2
@@ -39,8 +39,8 @@ export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable, OnChan
|
||||
@Input('label') textLabel: string = '';
|
||||
|
||||
/** The portal that will be the hosted content of the tab */
|
||||
private _contentPortal: TemplatePortal | null = null;
|
||||
get content(): TemplatePortal | null { return this._contentPortal; }
|
||||
private _contentPortal: TemplatePortal<any> | null = null;
|
||||
get content(): TemplatePortal<any> | null { return this._contentPortal; }
|
||||
|
||||
/** Emits whenever the label changes. */
|
||||
_labelChange = new Subject<void>();
|
||||
|
||||
Reference in New Issue
Block a user