diff --git a/src/cdk/portal/dom-portal-outlet.ts b/src/cdk/portal/dom-portal-outlet.ts index af0530c7906a..811d14b6a4ea 100644 --- a/src/cdk/portal/dom-portal-outlet.ts +++ b/src/cdk/portal/dom-portal-outlet.ts @@ -10,6 +10,7 @@ import { ApplicationRef, ComponentRef, EmbeddedViewRef, + EnvironmentInjector, Injector, NgModuleRef, createComponent, @@ -65,19 +66,22 @@ export class DomPortalOutlet extends BasePortalOutlet { if ((typeof ngDevMode === 'undefined' || ngDevMode) && !this._appRef) { throw Error('Cannot attach component portal to outlet without an ApplicationRef.'); } + const appRef = this._appRef!; + const elementInjector = portal.injector || this._defaultInjector || Injector.NULL; + const environmentInjector = elementInjector.get(EnvironmentInjector, appRef.injector); componentRef = createComponent(portal.component, { - elementInjector: portal.injector || this._defaultInjector || Injector.NULL, - environmentInjector: this._appRef!.injector, + elementInjector, + environmentInjector, projectableNodes: portal.projectableNodes || undefined, }); - this._appRef!.attachView(componentRef.hostView); + appRef.attachView(componentRef.hostView); this.setDisposeFn(() => { // Verify that the ApplicationRef has registered views before trying to detach a host view. // This check also protects the `detachView` from being called on a destroyed ApplicationRef. - if (this._appRef!.viewCount > 0) { - this._appRef!.detachView(componentRef.hostView); + if (appRef.viewCount > 0) { + appRef.detachView(componentRef.hostView); } componentRef.destroy(); }); diff --git a/src/cdk/portal/portal.spec.ts b/src/cdk/portal/portal.spec.ts index 24b5f89da711..44f4a37992d0 100644 --- a/src/cdk/portal/portal.spec.ts +++ b/src/cdk/portal/portal.spec.ts @@ -5,12 +5,15 @@ import { ComponentRef, Directive, ElementRef, + EnvironmentInjector, Injector, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, + createComponent, + createEnvironmentInjector, inject, } from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; @@ -614,6 +617,9 @@ describe('Portals', () => { expect(someDomElement.textContent) .withContext('Expected the static string "Pizza" in the DomPortalOutlet.') .toContain('Pizza'); + expect(someDomElement.textContent) + .withContext('Did not expect the bound string "Chocolate" in the DomPortalOutlet') + .not.toContain('Chocolate'); componentInstance.snack = new Chocolate(); someFixture.detectChanges(); @@ -628,6 +634,28 @@ describe('Portals', () => { .toBe(''); }); + it('should support a component portal with element injector that has a child environment injector as parent', () => { + // https://github.com/angular/components/issues/30609 + const childEnvironment = createEnvironmentInjector( + [Chocolate], + someInjector.get(EnvironmentInjector), + 'Child environment', + ); + + @Component({template: ''}) + class ChildComponent {} + + const component = createComponent(ChildComponent, { + environmentInjector: childEnvironment, + }); + const portal = new ComponentPortal(PizzaMsg, null, component.injector); + + const componentInstance = portal.attach(host).instance; + expect(componentInstance.snack) + .withContext('Expected Chocolate to have been injected') + .toBeInstanceOf(Chocolate); + }); + it('should call the dispose function even if the host has no attached content', () => { let spy = jasmine.createSpy('host dispose spy');