Skip to content

Commit 90a6ac9

Browse files
shlomiassafkara
authored andcommitted
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`)
1 parent cdbf305 commit 90a6ac9

16 files changed

+155
-41
lines changed

src/cdk/overlay/overlay-directives.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export class OverlayOrigin {
9393
})
9494
export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
9595
private _overlayRef: OverlayRef;
96-
private _templatePortal: TemplatePortal;
96+
private _templatePortal: TemplatePortal<any>;
9797
private _hasBackdrop = false;
9898
private _backdropSubscription: Subscription | null;
9999
private _positionSubscription: Subscription;

src/cdk/overlay/overlay.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
describe('Overlay', () => {
2121
let overlay: Overlay;
2222
let componentPortal: ComponentPortal<PizzaMsg>;
23-
let templatePortal: TemplatePortal;
23+
let templatePortal: TemplatePortal<any>;
2424
let overlayContainerElement: HTMLElement;
2525
let viewContainerFixture: ComponentFixture<TestComponentWithTemplatePortals>;
2626

src/cdk/portal/dom-portal-host.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ export class DomPortalHost extends BasePortalHost {
6969
* Attaches a template portal to the DOM as an embedded view.
7070
* @param portal Portal to be attached.
7171
*/
72-
attachTemplatePortal(portal: TemplatePortal): Map<string, any> {
72+
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
7373
let viewContainer = portal.viewContainerRef;
74-
let viewRef = viewContainer.createEmbeddedView(portal.templateRef);
74+
let viewRef = viewContainer.createEmbeddedView(portal.templateRef, portal.context);
7575
viewRef.detectChanges();
7676

7777
// The method `createEmbeddedView` will add the view as a child of the viewContainer.
@@ -87,7 +87,7 @@ export class DomPortalHost extends BasePortalHost {
8787
}));
8888

8989
// TODO(jelbourn): Return locals from view.
90-
return new Map<string, any>();
90+
return viewRef;
9191
}
9292

9393
/**

src/cdk/portal/portal-directives.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
NgModule,
1111
ComponentRef,
1212
Directive,
13+
EmbeddedViewRef,
1314
TemplateRef,
1415
ComponentFactoryResolver,
1516
ViewContainerRef,
@@ -32,7 +33,7 @@ import {Portal, TemplatePortal, ComponentPortal, BasePortalHost} from './portal'
3233
selector: '[cdk-portal], [cdkPortal], [portal]',
3334
exportAs: 'cdkPortal',
3435
})
35-
export class TemplatePortalDirective extends TemplatePortal {
36+
export class TemplatePortalDirective extends TemplatePortal<any> {
3637
constructor(templateRef: TemplateRef<any>, viewContainerRef: ViewContainerRef) {
3738
super(templateRef, viewContainerRef);
3839
}
@@ -117,16 +118,14 @@ export class PortalHostDirective extends BasePortalHost implements OnDestroy {
117118
* Attach the given TemplatePortal to this PortlHost as an embedded View.
118119
* @param portal Portal to be attached.
119120
*/
120-
attachTemplatePortal(portal: TemplatePortal): Map<string, any> {
121+
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
121122
portal.setAttachedHost(this);
122-
123-
this._viewContainerRef.createEmbeddedView(portal.templateRef);
123+
const viewRef = this._viewContainerRef.createEmbeddedView(portal.templateRef, portal.context);
124124
super.setDisposeFn(() => this._viewContainerRef.clear());
125125

126126
this._portal = portal;
127127

128-
// TODO(jelbourn): return locals from view
129-
return new Map<string, any>();
128+
return viewRef;
130129
}
131130
}
132131

src/cdk/portal/portal.spec.ts

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import {
1010
Optional,
1111
Injector,
1212
ApplicationRef,
13+
TemplateRef
1314
} from '@angular/core';
1415
import {CommonModule} from '@angular/common';
1516
import {TemplatePortalDirective, PortalHostDirective, PortalModule} from './portal-directives';
16-
import {Portal, ComponentPortal} from './portal';
17+
import {Portal, ComponentPortal, TemplatePortal} from './portal';
1718
import {DomPortalHost} from './dom-portal-host';
1819

1920

@@ -45,6 +46,57 @@ describe('Portals', () => {
4546
expect(hostContainer.textContent).toContain('Pizza');
4647
});
4748

49+
it('should load a template into the portal', () => {
50+
let testAppComponent = fixture.debugElement.componentInstance;
51+
let hostContainer = fixture.nativeElement.querySelector('.portal-container');
52+
53+
let templatePortal = new TemplatePortal(testAppComponent.templateRef, null!);
54+
testAppComponent.selectedPortal = templatePortal;
55+
fixture.detectChanges();
56+
// Expect that the content of the attached portal is present and no context is projected
57+
expect(hostContainer.textContent).toContain('Banana');
58+
});
59+
60+
it('should project template context bindings in the portal', () => {
61+
let testAppComponent = fixture.debugElement.componentInstance;
62+
let hostContainer = fixture.nativeElement.querySelector('.portal-container');
63+
64+
// TemplatePortal without context:
65+
let templatePortal = new TemplatePortal(testAppComponent.templateRef, null!);
66+
testAppComponent.selectedPortal = templatePortal;
67+
fixture.detectChanges();
68+
// Expect that the content of the attached portal is present and NO context is projected
69+
expect(hostContainer.textContent).toContain('Banana - !');
70+
71+
// using TemplatePortal.attach method to set context
72+
testAppComponent.selectedPortal = undefined;
73+
fixture.detectChanges();
74+
templatePortal.attach(testAppComponent.portalHost, {$implicit: {status: 'rotten'}});
75+
fixture.detectChanges();
76+
// Expect that the content of the attached portal is present and context given via the
77+
// attach method is projected
78+
expect(hostContainer.textContent).toContain('Banana - rotten!');
79+
80+
// using TemplatePortal constructor to set the context
81+
templatePortal =
82+
new TemplatePortal(testAppComponent.templateRef, null!, {$implicit: {status: 'fresh'}});
83+
testAppComponent.selectedPortal = templatePortal;
84+
fixture.detectChanges();
85+
// Expect that the content of the attached portal is present and context given via the
86+
// constructor is projected
87+
expect(hostContainer.textContent).toContain('Banana - fresh!');
88+
89+
// using TemplatePortal constructor to set the context but also calling attach method with
90+
// context, the latter should take precedence:
91+
testAppComponent.selectedPortal = undefined;
92+
fixture.detectChanges();
93+
templatePortal.attach(testAppComponent.portalHost, {$implicit: {status: 'rotten'}});
94+
fixture.detectChanges();
95+
// Expect that the content of the attached portal is present and and context given via the
96+
// attach method is projected and get precedence over constructor context
97+
expect(hostContainer.textContent).toContain('Banana - rotten!');
98+
});
99+
48100
it('should dispose the host when destroyed', () => {
49101
// Set the selectedHost to be a ComponentPortal.
50102
let testAppComponent = fixture.debugElement.componentInstance;
@@ -299,15 +351,15 @@ describe('Portals', () => {
299351
fixture.detectChanges();
300352

301353
// Attach the TemplatePortal.
302-
testAppComponent.portalWithBinding.attach(host);
354+
testAppComponent.portalWithBinding.attach(host, {$implicit: {status: 'fresh'}});
303355
fixture.detectChanges();
304356

305357
// Now that the portal is attached, change detection has to happen again in order
306358
// for the bindings to update.
307359
fixture.detectChanges();
308360

309361
// Expect that the content of the attached portal is present.
310-
expect(someDomElement.textContent).toContain('Banana');
362+
expect(someDomElement.textContent).toContain('Banana - fresh');
311363

312364
// When updating the binding value.
313365
testAppComponent.fruit = 'Mango';
@@ -416,18 +468,22 @@ class ArbitraryViewContainerRefComponent {
416468
<ng-template cdk-portal>Cake</ng-template>
417469
418470
<div *cdk-portal>Pie</div>
419-
<ng-template cdk-portal> {{fruit}} </ng-template>
471+
<ng-template cdk-portal let-data> {{fruit}} - {{ data?.status }} </ng-template>
420472
421473
<ng-template cdk-portal>
422474
<ul>
423475
<li *ngFor="let fruitName of fruits"> {{fruitName}} </li>
424476
</ul>
425477
</ng-template>
478+
479+
<ng-template #templateRef let-data> {{fruit}} - {{ data?.status }}!</ng-template>
426480
`,
427481
})
428482
class PortalTestApp {
429483
@ViewChildren(TemplatePortalDirective) portals: QueryList<TemplatePortalDirective>;
430484
@ViewChild(PortalHostDirective) portalHost: PortalHostDirective;
485+
@ViewChild('templateRef', { read: TemplateRef }) templateRef: TemplateRef<any>;
486+
431487
selectedPortal: Portal<any>;
432488
fruit: string = 'Banana';
433489
fruits = ['Apple', 'Pineapple', 'Durian'];
@@ -449,6 +505,7 @@ class PortalTestApp {
449505
get portalWithTemplate() {
450506
return this.portals.toArray()[3];
451507
}
508+
452509
}
453510

454511
// Create a real (non-test) NgModule as a workaround for

src/cdk/portal/portal.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ViewContainerRef,
1212
ElementRef,
1313
ComponentRef,
14+
EmbeddedViewRef,
1415
Injector
1516
} from '@angular/core';
1617
import {
@@ -103,42 +104,43 @@ export class ComponentPortal<T> extends Portal<ComponentRef<T>> {
103104
}
104105
}
105106

106-
107107
/**
108108
* A `TemplatePortal` is a portal that represents some embedded template (TemplateRef).
109109
*/
110-
export class TemplatePortal extends Portal<Map<string, any>> {
110+
export class TemplatePortal<C> extends Portal<C> {
111111
/** The embedded template that will be used to instantiate an embedded View in the host. */
112-
templateRef: TemplateRef<any>;
112+
templateRef: TemplateRef<C>;
113113

114114
/** Reference to the ViewContainer into which the template will be stamped out. */
115115
viewContainerRef: ViewContainerRef;
116116

117-
/**
118-
* Additional locals for the instantiated embedded view.
119-
* These locals can be seen as "exports" for the template, such as how ngFor has
120-
* index / event / odd.
121-
* See https://angular.io/docs/ts/latest/api/core/EmbeddedViewRef-class.html
122-
*/
123-
locals: Map<string, any> = new Map<string, any>();
117+
context: C | undefined;
124118

125-
constructor(template: TemplateRef<any>, viewContainerRef: ViewContainerRef) {
119+
constructor(template: TemplateRef<any>, viewContainerRef: ViewContainerRef, context?: C) {
126120
super();
127121
this.templateRef = template;
128122
this.viewContainerRef = viewContainerRef;
123+
if (context) {
124+
this.context = context;
125+
}
129126
}
130127

131128
get origin(): ElementRef {
132129
return this.templateRef.elementRef;
133130
}
134131

135-
attach(host: PortalHost, locals?: Map<string, any>): Map<string, any> {
136-
this.locals = locals == null ? new Map<string, any>() : locals;
132+
/**
133+
* Attach the the portal to the provided `PortalHost`.
134+
* When a context is provided it will override the `context` property of the `TemplatePortal`
135+
* instance.
136+
*/
137+
attach(host: PortalHost, context: C | undefined = this.context): C {
138+
this.context = context;
137139
return super.attach(host);
138140
}
139141

140142
detach(): void {
141-
this.locals = new Map<string, any>();
143+
this.context = undefined;
142144
return super.detach();
143145
}
144146
}
@@ -203,7 +205,7 @@ export abstract class BasePortalHost implements PortalHost {
203205

204206
abstract attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;
205207

206-
abstract attachTemplatePortal(portal: TemplatePortal): Map<string, any>;
208+
abstract attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;
207209

208210
detach(): void {
209211
if (this._attachedPortal) {

src/demo-app/dialog/dialog-demo.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ <h2>Other options</h2>
7878
<p>Last afterClosed result: {{lastAfterClosedResult}}</p>
7979
<p>Last beforeClose result: {{lastBeforeCloseResult}}</p>
8080

81-
<ng-template>
81+
<ng-template let-data let-dialogRef="dialogRef">
8282
I'm a template dialog. I've been opened {{numTemplateOpens}} times!
83+
84+
<p>It's Jazz!</p>
85+
86+
<md-input-container>
87+
<input mdInput placeholder="How much?" #howMuch>
88+
</md-input-container>
89+
90+
<p> {{ data.message }} </p>
91+
<button type="button" (click)="dialogRef.close(lastCloseResult = howMuch.value)">Close dialog</button>
92+
<button (click)="dialogRef.updateSize('500px', '500px').updatePosition({ top: '25px', left: '25px' });">Change dimensions</button>`
8393
</ng-template>

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export function getMdAutocompleteMissingPanelError(): Error {
118118
})
119119
export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
120120
private _overlayRef: OverlayRef | null;
121-
private _portal: TemplatePortal;
121+
private _portal: TemplatePortal<any>;
122122
private _panelOpen: boolean = false;
123123

124124
/** Strategy that is used to position the panel. */

src/lib/dialog/dialog-container.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Component,
1111
ComponentRef,
1212
ElementRef,
13+
EmbeddedViewRef,
1314
EventEmitter,
1415
Inject,
1516
NgZone,
@@ -124,7 +125,7 @@ export class MdDialogContainer extends BasePortalHost {
124125
* Attach a TemplatePortal as content to this dialog container.
125126
* @param portal Portal to be attached as the dialog content.
126127
*/
127-
attachTemplatePortal(portal: TemplatePortal): Map<string, any> {
128+
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
128129
if (this._portalHost.hasAttached()) {
129130
throwMdDialogContentAlreadyAttachedError();
130131
}

src/lib/dialog/dialog.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Injector,
1717
Inject,
1818
ChangeDetectionStrategy,
19+
TemplateRef
1920
} from '@angular/core';
2021
import {By} from '@angular/platform-browser';
2122
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
@@ -79,6 +80,28 @@ describe('MdDialog', () => {
7980
expect(dialogContainerElement.getAttribute('role')).toBe('dialog');
8081
});
8182

83+
it('should open a dialog with a template', () => {
84+
const templateRefFixture = TestBed.createComponent(ComponentWithTemplateRef);
85+
templateRefFixture.componentInstance.localValue = 'Bees';
86+
templateRefFixture.detectChanges();
87+
88+
const data = {value: 'Knees'};
89+
90+
let dialogRef = dialog.open(templateRefFixture.componentInstance.templateRef, { data });
91+
92+
viewContainerFixture.detectChanges();
93+
94+
expect(overlayContainerElement.textContent).toContain('Cheese Bees Knees');
95+
expect(templateRefFixture.componentInstance.dialogRef).toBe(dialogRef);
96+
97+
viewContainerFixture.detectChanges();
98+
99+
let dialogContainerElement = overlayContainerElement.querySelector('md-dialog-container')!;
100+
expect(dialogContainerElement.getAttribute('role')).toBe('dialog');
101+
102+
dialogRef.close();
103+
});
104+
82105
it('should use injector from viewContainerRef for DialogInjector', () => {
83106
let dialogRef = dialog.open(PizzaMsg, {
84107
viewContainerRef: testViewContainerRef
@@ -876,6 +899,23 @@ class ComponentWithChildViewContainer {
876899
}
877900
}
878901

902+
@Component({
903+
selector: 'arbitrary-component-with-template-ref',
904+
template: `<ng-template let-data let-dialogRef="dialogRef">
905+
Cheese {{localValue}} {{data?.value}}{{setDialogRef(dialogRef)}}</ng-template>`,
906+
})
907+
class ComponentWithTemplateRef {
908+
localValue: string;
909+
dialogRef: MdDialogRef<any>;
910+
911+
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
912+
913+
setDialogRef(dialogRef: MdDialogRef<any>): string {
914+
this.dialogRef = dialogRef;
915+
return '';
916+
}
917+
}
918+
879919
/** Simple component for testing ComponentPortal. */
880920
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
881921
class PizzaMsg {
@@ -916,6 +956,7 @@ class DialogWithInjectedData {
916956
// https://github.com/angular/angular/issues/10760
917957
const TEST_DIRECTIVES = [
918958
ComponentWithChildViewContainer,
959+
ComponentWithTemplateRef,
919960
PizzaMsg,
920961
DirectiveWithViewContainer,
921962
ComponentWithOnPushViewContainer,
@@ -929,6 +970,7 @@ const TEST_DIRECTIVES = [
929970
declarations: TEST_DIRECTIVES,
930971
entryComponents: [
931972
ComponentWithChildViewContainer,
973+
ComponentWithTemplateRef,
932974
PizzaMsg,
933975
ContentElementDialog,
934976
DialogWithInjectedData

0 commit comments

Comments
 (0)