From cf10b336e7c861155bfd72042e486b4cdd806790 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Mon, 28 Oct 2019 23:40:16 -0700 Subject: [PATCH] fix(ivy): ComponentFactory.create should clear host element content (#33487) Prior to this change, ComponentFactory.create function invocation in Ivy retained the content of the host element (in case host element reference or CSS seelctor is provided as an argument). This behavior is different in View Engine, where the content of the host element was cleared, except for the case when ShadowDom encapsulation is used (to make sure native slot projection works). This commit aligns Ivy and View Engine and makes sure the host element is cleared before component content insertion. PR Close #33487 --- .../compiler/src/compiler_facade_interface.ts | 7 +- .../src/compiler/compiler_facade_interface.ts | 7 +- packages/core/src/render3/component.ts | 3 +- packages/core/src/render3/component_ref.ts | 2 +- .../core/src/render3/instructions/shared.ts | 53 ++++++++---- .../core/src/render3/interfaces/renderer.ts | 3 +- .../core/test/acceptance/component_spec.ts | 85 ++++++++++++++++++- packages/core/test/render3/render_util.ts | 3 +- 8 files changed, 139 insertions(+), 24 deletions(-) diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index c084bed77b..75c7202529 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -167,7 +167,12 @@ export interface R3FactoryDefMetadataFacade { target: R3FactoryTarget; } -export type ViewEncapsulation = number; +export enum ViewEncapsulation { + Emulated = 0, + Native = 1, + None = 2, + ShadowDom = 3 +} export type ChangeDetectionStrategy = number; diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index c084bed77b..75c7202529 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -167,7 +167,12 @@ export interface R3FactoryDefMetadataFacade { target: R3FactoryTarget; } -export type ViewEncapsulation = number; +export enum ViewEncapsulation { + Emulated = 0, + Native = 1, + None = 2, + ShadowDom = 3 +} export type ChangeDetectionStrategy = number; diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 8fc900c9e0..a6d3872597 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -118,7 +118,8 @@ export function renderComponent( // The first index of the first selector is the tag name. const componentTag = componentDef.selectors ![0] ![0] as string; - const hostRNode = locateHostElement(rendererFactory, opts.host || componentTag); + const hostRNode = + locateHostElement(rendererFactory, opts.host || componentTag, componentDef.encapsulation); const rootFlags = componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot : LViewFlags.CheckAlways | LViewFlags.IsRoot; const rootContext = createRootContext(opts.scheduler, opts.playerHandler); diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 287498c0d9..ecb71c3c9f 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -134,7 +134,7 @@ export class ComponentFactory extends viewEngine_ComponentFactory { const sanitizer = rootViewInjector.get(Sanitizer, null); const hostRNode = rootSelectorOrNode ? - locateHostElement(rendererFactory, rootSelectorOrNode) : + locateHostElement(rendererFactory, rootSelectorOrNode, this.componentDef.encapsulation) : elementCreate(this.selector, rendererFactory.createRenderer(null, this.componentDef), null); const rootFlags = this.componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot : diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 79dbfb9be5..286ddb4ddd 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -8,6 +8,7 @@ import {Injector} from '../../di'; import {ErrorHandler} from '../../error_handler'; import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from '../../metadata/schema'; +import {ViewEncapsulation} from '../../metadata/view'; import {validateAgainstEventAttributes, validateAgainstEventProperties} from '../../sanitization/sanitization'; import {Sanitizer} from '../../sanitization/sanitizer'; import {assertDataInRange, assertDefined, assertDomNode, assertEqual, assertGreaterThan, assertNotEqual, assertNotSame} from '../../util/assert'; @@ -678,32 +679,50 @@ function createViewBlueprint(bindingStartIndex: number, initialViewLength: numbe return blueprint as LView; } -export function createError(text: string, token: any) { +function createError(text: string, token: any) { return new Error(`Renderer: ${text} [${stringifyForError(token)}]`); } - -/** - * Locates the host native element, used for bootstrapping existing nodes into rendering pipeline. - * - * @param elementOrSelector Render element or CSS selector to locate the element. - */ -export function locateHostElement( - factory: RendererFactory3, elementOrSelector: RElement | string): RElement|null { - const defaultRenderer = factory.createRenderer(null, null); - const rNode = typeof elementOrSelector === 'string' ? - (isProceduralRenderer(defaultRenderer) ? - defaultRenderer.selectRootElement(elementOrSelector) : - defaultRenderer.querySelector(elementOrSelector)) : - elementOrSelector; - if (ngDevMode && !rNode) { +function assertHostNodeExists(rElement: RElement, elementOrSelector: RElement | string) { + if (!rElement) { if (typeof elementOrSelector === 'string') { throw createError('Host node with selector not found:', elementOrSelector); } else { throw createError('Host node is required:', elementOrSelector); } } - return rNode; +} + +/** + * Locates the host native element, used for bootstrapping existing nodes into rendering pipeline. + * + * @param rendererFactory Factory function to create renderer instance. + * @param elementOrSelector Render element or CSS selector to locate the element. + * @param encapsulation View Encapsulation defined for component that requests host element. + */ +export function locateHostElement( + rendererFactory: RendererFactory3, elementOrSelector: RElement | string, + encapsulation: ViewEncapsulation): RElement { + const renderer = rendererFactory.createRenderer(null, null); + + if (isProceduralRenderer(renderer)) { + // When using native Shadow DOM, do not clear host element to allow native slot projection + const preserveContent = encapsulation === ViewEncapsulation.ShadowDom; + return renderer.selectRootElement(elementOrSelector, preserveContent); + } + + let rElement = typeof elementOrSelector === 'string' ? + renderer.querySelector(elementOrSelector) ! : + elementOrSelector; + ngDevMode && assertHostNodeExists(rElement, elementOrSelector); + + // Always clear host element's content when Renderer3 is in use. For procedural renderer case we + // make it depend on whether ShadowDom encapsulation is used (in which case the content should be + // preserved to allow native slot projection). ShadowDom encapsulation requires procedural + // renderer, and procedural renderer case is handled above. + rElement.textContent = ''; + + return rElement; } /** diff --git a/packages/core/src/render3/interfaces/renderer.ts b/packages/core/src/render3/interfaces/renderer.ts index fac99d5323..a61d7c4a90 100644 --- a/packages/core/src/render3/interfaces/renderer.ts +++ b/packages/core/src/render3/interfaces/renderer.ts @@ -75,7 +75,7 @@ export interface ProceduralRenderer3 { appendChild(parent: RElement, newChild: RNode): void; insertBefore(parent: RNode, newChild: RNode, refChild: RNode|null): void; removeChild(parent: RElement, oldChild: RNode, isHostElement?: boolean): void; - selectRootElement(selectorOrNode: string|any): RElement; + selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): RElement; parentNode(node: RNode): RElement|null; nextSibling(node: RNode): RNode|null; @@ -155,6 +155,7 @@ export interface RElement extends RNode { style: RCssStyleDeclaration; classList: RDomTokenList; className: string; + textContent: string|null; setAttribute(name: string, value: string): void; removeAttribute(name: string): void; setAttributeNS(namespaceURI: string, qualifiedName: string, value: string): void; diff --git a/packages/core/test/acceptance/component_spec.ts b/packages/core/test/acceptance/component_spec.ts index 4283fc4618..f80ac4c3a0 100644 --- a/packages/core/test/acceptance/component_spec.ts +++ b/packages/core/test/acceptance/component_spec.ts @@ -6,9 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ComponentFactoryResolver, ComponentRef, ElementRef, InjectionToken, Injector, Input, NgModule, OnDestroy, Renderer2, Type, ViewChild, ViewContainerRef, ViewEncapsulation} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; +import {Component, ComponentFactoryResolver, ComponentRef, ElementRef, InjectionToken, Injector, Input, NgModule, OnDestroy, Renderer2, RendererFactory2, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ɵsetDocument} from '@angular/core'; import {TestBed} from '@angular/core/testing'; +import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {onlyInIvy} from '@angular/private/testing'; + +import {domRendererFactory3} from '../../src/render3/interfaces/renderer'; describe('component', () => { @@ -335,4 +340,82 @@ describe('component', () => { expect(log).toEqual(['CompB:ngDoCheck']); }); + describe('should clear host element if provided in ComponentFactory.create', () => { + function runTestWithRenderer(rendererProviders: any[]) { + @Component({ + selector: 'dynamic-comp', + template: 'DynamicComponent Content', + }) + class DynamicComponent { + } + + @Component({ + selector: 'app', + template: ` +
+ Existing content in slot A, which includes some HTML elements. +
+
+

+ Existing content in slot B, which includes some HTML elements. +

+
+ `, + }) + class App { + constructor(public injector: Injector, public cfr: ComponentFactoryResolver) {} + + createDynamicComponent(target: any) { + const dynamicCompFactory = this.cfr.resolveComponentFactory(DynamicComponent); + dynamicCompFactory.create(this.injector, [], target); + } + } + + // View Engine requires DynamicComponent to be in entryComponents. + @NgModule({ + declarations: [App, DynamicComponent], + entryComponents: [App, DynamicComponent], + }) + class AppModule { + } + + function _document(): any { + // Tell Ivy about the global document + ɵsetDocument(document); + return document; + } + + TestBed.configureTestingModule({ + imports: [AppModule], + providers: [ + {provide: DOCUMENT, useFactory: _document, deps: []}, + rendererProviders, + ], + }); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + // Create an instance of DynamicComponent and provide host element *reference* + let targetEl = document.getElementById('dynamic-comp-root-a') !; + fixture.componentInstance.createDynamicComponent(targetEl); + fixture.detectChanges(); + expect(targetEl.innerHTML).not.toContain('Existing content in slot A'); + expect(targetEl.innerHTML).toContain('DynamicComponent Content'); + + // Create an instance of DynamicComponent and provide host element *selector* + targetEl = document.getElementById('dynamic-comp-root-b') !; + fixture.componentInstance.createDynamicComponent('#dynamic-comp-root-b'); + fixture.detectChanges(); + expect(targetEl.innerHTML).not.toContain('Existing content in slot B'); + expect(targetEl.innerHTML).toContain('DynamicComponent Content'); + } + + it('with Renderer2', + () => runTestWithRenderer([{provide: RendererFactory2, useClass: DomRendererFactory2}])); + + onlyInIvy('Renderer3 is supported only in Ivy') + .it('with Renderer3', () => runTestWithRenderer( + [{provide: RendererFactory2, useValue: domRendererFactory3}])); + }); }); diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 479fd071be..5f3fb49a52 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -465,7 +465,8 @@ class MockRenderer implements ProceduralRenderer3 { } removeChild(parent: RElement, oldChild: RNode): void { parent.removeChild(oldChild); } selectRootElement(selectorOrNode: string|any): RElement { - return ({} as any); + return typeof selectorOrNode === 'string' ? document.querySelector(selectorOrNode) : + selectorOrNode; } parentNode(node: RNode): RElement|null { return node.parentNode as RElement; } nextSibling(node: RNode): RNode|null { return node.nextSibling; }