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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user