fix(ivy): injecting incorrect Renderer2 into child components (#31063)
In ViewEngine injecting a Renderer2 returns a renderer that is specific to the particular component, however in Ivy we inject the renderer for the parent view instead. This causes it to set incorrect `ngcontent` attributes when creating elements through the renderer. The issue comes from the fact that the `Renderer2` is created according to the current `LView`, but because DI happens before we've entered the `LView` of the component that's injecting the renderer, we end up with one that's one level up. We work around the issue by finding the `LView` that corresponds to the `previousOrParentTNode` inside of the parent view and associating the `Renderer2` with it. This PR resolves FW-1382. PR Close #31063
This commit is contained in:
parent
6f5d910ddd
commit
fcb03abc72
|
@ -29,7 +29,7 @@ import {getParentInjectorTNode} from './node_util';
|
||||||
import {getLView, getPreviousOrParentTNode} from './state';
|
import {getLView, getPreviousOrParentTNode} from './state';
|
||||||
import {getParentInjectorView, hasParentInjector} from './util/injector_utils';
|
import {getParentInjectorView, hasParentInjector} from './util/injector_utils';
|
||||||
import {findComponentView} from './util/view_traversal_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';
|
import {ViewRef} from './view_ref';
|
||||||
|
|
||||||
|
|
||||||
|
@ -389,6 +389,7 @@ export function createViewRef(
|
||||||
return null !;
|
return null !;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a Renderer2 (or throws when application was bootstrapped with Renderer3) */
|
||||||
function getOrCreateRenderer2(view: LView): Renderer2 {
|
function getOrCreateRenderer2(view: LView): Renderer2 {
|
||||||
const renderer = view[RENDERER];
|
const renderer = view[RENDERER];
|
||||||
if (isProceduralRenderer(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 {
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {TestBed} from '@angular/core/testing';
|
||||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
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');
|
'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: '<parent-comp></parent-comp>',
|
||||||
|
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: '<child></child>',
|
||||||
|
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.');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue