diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts index 10e6cc750c..d7a06ed835 100644 --- a/packages/core/src/render3/view_engine_compatibility.ts +++ b/packages/core/src/render3/view_engine_compatibility.ts @@ -29,7 +29,7 @@ import {getParentInjectorTNode} from './node_util'; import {getLView, getPreviousOrParentTNode} from './state'; import {getParentInjectorView, hasParentInjector} from './util/injector_utils'; import {findComponentView} from './util/view_traversal_utils'; -import {getComponentViewByIndex, getNativeByTNode, isComponent, isLContainer, isRootView, unwrapRNode, viewAttachedToContainer} from './util/view_utils'; +import {getComponentViewByIndex, getNativeByTNode, isComponent, isLContainer, isLView, isRootView, unwrapRNode, viewAttachedToContainer} from './util/view_utils'; import {ViewRef} from './view_ref'; @@ -389,6 +389,7 @@ export function createViewRef( return null !; } +/** Returns a Renderer2 (or throws when application was bootstrapped with Renderer3) */ function getOrCreateRenderer2(view: LView): Renderer2 { const renderer = view[RENDERER]; if (isProceduralRenderer(renderer)) { @@ -398,7 +399,12 @@ function getOrCreateRenderer2(view: LView): Renderer2 { } } -/** Returns a Renderer2 (or throws when application was bootstrapped with Renderer3) */ +/** Injects a Renderer2 for the current component. */ export function injectRenderer2(): Renderer2 { - return getOrCreateRenderer2(getLView()); + // We need the Renderer to be based on the component that it's being injected into, however since + // DI happens before we've entered its view, `getLView` will return the parent view instead. + const lView = getLView(); + const tNode = getPreviousOrParentTNode(); + const nodeAtIndex = getComponentViewByIndex(tNode.index, lView); + return getOrCreateRenderer2(isLView(nodeAtIndex) ? nodeAtIndex : lView); } diff --git a/packages/core/test/acceptance/component_spec.ts b/packages/core/test/acceptance/component_spec.ts index 7fe1e54939..be2040bdb6 100644 --- a/packages/core/test/acceptance/component_spec.ts +++ b/packages/core/test/acceptance/component_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ComponentFactoryResolver, ComponentRef, InjectionToken, NgModule, OnDestroy, Type, ViewChild, ViewContainerRef, ViewEncapsulation} from '@angular/core'; +import {Component, ComponentFactoryResolver, ComponentRef, ElementRef, InjectionToken, NgModule, OnDestroy, Renderer2, Type, ViewChild, ViewContainerRef, ViewEncapsulation} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -178,4 +178,79 @@ describe('component', () => { 'Expected component onDestroy method to be called when its parent view is destroyed'); }); }); + + it('should use a new ngcontent attribute for child elements created w/ Renderer2', () => { + @Component({ + selector: 'app-root', + template: '', + styles: [':host { color: red; }'], // `styles` must exist for encapsulation to apply. + encapsulation: ViewEncapsulation.Emulated, + }) + class AppRoot { + } + + @Component({ + selector: 'parent-comp', + template: '', + styles: [':host { color: orange; }'], // `styles` must exist for encapsulation to apply. + encapsulation: ViewEncapsulation.Emulated, + }) + class ParentComponent { + constructor(elementRef: ElementRef, renderer: Renderer2) { + const elementFromRenderer = renderer.createElement('p'); + renderer.appendChild(elementRef.nativeElement, elementFromRenderer); + } + } + + TestBed.configureTestingModule({declarations: [AppRoot, ParentComponent]}); + const fixture = TestBed.createComponent(AppRoot); + fixture.detectChanges(); + + const secondParentEl: HTMLElement = fixture.nativeElement.querySelector('parent-comp'); + const elementFromRenderer: HTMLElement = fixture.nativeElement.querySelector('p'); + const getNgContentAttr = (element: HTMLElement) => { + return Array.from(element.attributes).map(a => a.name).find(a => /ngcontent/.test(a)); + }; + + const hostNgContentAttr = getNgContentAttr(secondParentEl); + const viewNgContentAttr = getNgContentAttr(elementFromRenderer); + + expect(hostNgContentAttr) + .not.toBe( + viewNgContentAttr, + 'Expected child manually created via Renderer2 to have a different view encapsulation' + + 'attribute than its host element'); + }); + + it('should create a new Renderer2 for each component', () => { + @Component({ + selector: 'child', + template: '', + styles: [':host { color: red; }'], + encapsulation: ViewEncapsulation.Emulated, + }) + class Child { + constructor(public renderer: Renderer2) {} + } + + @Component({ + template: '', + styles: [':host { color: orange; }'], + encapsulation: ViewEncapsulation.Emulated, + }) + class Parent { + @ViewChild(Child, {static: false}) childInstance !: Child; + constructor(public renderer: Renderer2) {} + } + + TestBed.configureTestingModule({declarations: [Parent, Child]}); + const fixture = TestBed.createComponent(Parent); + const componentInstance = fixture.componentInstance; + fixture.detectChanges(); + + // Assert like this, rather than `.not.toBe` so we get a better failure message. + expect(componentInstance.renderer !== componentInstance.childInstance.renderer) + .toBe(true, 'Expected renderers to be different.'); + }); + });