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; }