Skip to content

Commit 7fcb93b

Browse files
crisbetojelbourn
authored andcommitted
fix(portal-host): unable to clear and portal reference not being set (#3302)
* Fixes not being able to clear an attached `PortalHost` by setting its contents to `null`. * Fixes the `portal` reference not being set, if the portal host gets populated programmatically. * Clears references to the attached portal on destroy. * Switches to calling the inherited methods via `super`, instead of `this`, making it easier to spot which ones are inherited.
1 parent 22b0660 commit 7fcb93b

File tree

2 files changed

+63
-16
lines changed

2 files changed

+63
-16
lines changed

src/lib/core/portal/portal-directives.ts

+14-15
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,21 @@ export class PortalHostDirective extends BasePortalHost implements OnDestroy {
6363
return this._portal;
6464
}
6565

66-
set portal(p: Portal<any>) {
67-
if (p) {
68-
this._replaceAttachedPortal(p);
66+
set portal(portal: Portal<any>) {
67+
if (this.hasAttached()) {
68+
super.detach();
69+
}
70+
71+
if (portal) {
72+
super.attach(portal);
6973
}
74+
75+
this._portal = portal;
7076
}
7177

7278
ngOnDestroy() {
7379
super.dispose();
80+
this._portal = null;
7481
}
7582

7683
/**
@@ -94,6 +101,8 @@ export class PortalHostDirective extends BasePortalHost implements OnDestroy {
94101
portal.injector || viewContainerRef.parentInjector);
95102

96103
super.setDisposeFn(() => ref.destroy());
104+
this._portal = portal;
105+
97106
return ref;
98107
}
99108

@@ -107,21 +116,11 @@ export class PortalHostDirective extends BasePortalHost implements OnDestroy {
107116
this._viewContainerRef.createEmbeddedView(portal.templateRef);
108117
super.setDisposeFn(() => this._viewContainerRef.clear());
109118

119+
this._portal = portal;
120+
110121
// TODO(jelbourn): return locals from view
111122
return new Map<string, any>();
112123
}
113-
114-
/** Detaches the currently attached Portal (if there is one) and attaches the given Portal. */
115-
private _replaceAttachedPortal(p: Portal<any>): void {
116-
if (this.hasAttached()) {
117-
super.detach();
118-
}
119-
120-
if (p) {
121-
super.attach(p);
122-
this._portal = p;
123-
}
124-
}
125124
}
126125

127126

src/lib/core/portal/portal.spec.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {inject, ComponentFixture, TestBed, async} from '@angular/core/testing';
22
import {
33
NgModule,
44
Component,
5+
ViewChild,
56
ViewChildren,
67
QueryList,
78
ViewContainerRef,
@@ -10,7 +11,7 @@ import {
1011
Injector,
1112
ApplicationRef,
1213
} from '@angular/core';
13-
import {TemplatePortalDirective, PortalModule} from './portal-directives';
14+
import {TemplatePortalDirective, PortalHostDirective, PortalModule} from './portal-directives';
1415
import {Portal, ComponentPortal} from './portal';
1516
import {DomPortalHost} from './dom-portal-host';
1617

@@ -141,6 +142,52 @@ describe('Portals', () => {
141142

142143
expect(hostContainer.textContent).toContain('Pizza');
143144
});
145+
146+
it('should detach the portal when it is set to null', () => {
147+
let testAppComponent = fixture.debugElement.componentInstance;
148+
testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg);
149+
150+
fixture.detectChanges();
151+
expect(testAppComponent.portalHost.hasAttached()).toBe(true);
152+
expect(testAppComponent.portalHost.portal).toBe(testAppComponent.selectedPortal);
153+
154+
testAppComponent.selectedPortal = null;
155+
fixture.detectChanges();
156+
157+
expect(testAppComponent.portalHost.hasAttached()).toBe(false);
158+
expect(testAppComponent.portalHost.portal).toBeNull();
159+
});
160+
161+
it('should set the `portal` when attaching a component portal programmatically', () => {
162+
let testAppComponent = fixture.debugElement.componentInstance;
163+
let portal = new ComponentPortal(PizzaMsg);
164+
165+
testAppComponent.portalHost.attachComponentPortal(portal);
166+
167+
expect(testAppComponent.portalHost.portal).toBe(portal);
168+
});
169+
170+
it('should set the `portal` when attaching a template portal programmatically', () => {
171+
let testAppComponent = fixture.debugElement.componentInstance;
172+
fixture.detectChanges();
173+
174+
testAppComponent.portalHost.attachTemplatePortal(testAppComponent.cakePortal);
175+
176+
expect(testAppComponent.portalHost.portal).toBe(testAppComponent.cakePortal);
177+
});
178+
179+
it('should clear the portal reference on destroy', () => {
180+
let testAppComponent = fixture.debugElement.componentInstance;
181+
182+
testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg);
183+
fixture.detectChanges();
184+
185+
expect(testAppComponent.portalHost.portal).toBeTruthy();
186+
187+
fixture.destroy();
188+
189+
expect(testAppComponent.portalHost.portal).toBeNull();
190+
});
144191
});
145192

146193
describe('DomPortalHost', () => {
@@ -342,6 +389,7 @@ class ArbitraryViewContainerRefComponent {
342389
})
343390
class PortalTestApp {
344391
@ViewChildren(TemplatePortalDirective) portals: QueryList<TemplatePortalDirective>;
392+
@ViewChild(PortalHostDirective) portalHost: PortalHostDirective;
345393
selectedPortal: Portal<any>;
346394
fruit: string = 'Banana';
347395

0 commit comments

Comments
 (0)