diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 897111f680..a89bc8ff7e 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -159,4 +159,9 @@ export { bypassSanitizationTrustUrl as ɵbypassSanitizationTrustUrl, bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl, } from './sanitization/bypass'; + +export { + ElementContext as ɵElementContext, + getElementContext as ɵgetElementContext +} from './render3/element_discovery'; // clang-format on diff --git a/packages/core/src/render3/element_discovery.ts b/packages/core/src/render3/element_discovery.ts new file mode 100644 index 0000000000..d43244bfc2 --- /dev/null +++ b/packages/core/src/render3/element_discovery.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {RElement} from './interfaces/renderer'; +import {HEADER_OFFSET, LViewData} from './interfaces/view'; +import {StylingIndex} from './styling'; + +export const MONKEY_PATCH_KEY_NAME = '__ng_data__'; + +/** The internal element context which is specific to a given DOM node */ +export interface ElementContext { + /** The component\'s view data */ + lViewData: LViewData; + + /** The index of the element within the view data array */ + index: number; + + /** The instance of the DOM node */ + native: RElement; +} + +/** Returns the matching `ElementContext` data for a given DOM node. + * + * This function will examine the provided DOM element's monkey-patched property to figure out the + * associated index and view data (`LViewData`). + * + * If the monkey-patched value is the `LViewData` instance then the element context for that + * element will be created and the monkey-patch reference will be updated. Therefore when this + * function is called it may mutate the provided element\'s monkey-patch value. + * + * If the monkey-patch value is not detected then the code will walk up the DOM until an element + * is found which contains a monkey-patch reference. When that occurs then the provided element + * will be updated with a new context (which is then returned). + */ +export function getElementContext(element: RElement): ElementContext|null { + let context = (element as any)[MONKEY_PATCH_KEY_NAME] as ElementContext | LViewData | null; + if (context) { + if (Array.isArray(context)) { + const lViewData = context as LViewData; + const index = findMatchingElement(element, lViewData); + context = {index, native: element, lViewData}; + attachLViewDataToNode(element, context); + } + } else { + let parent = element as any; + while (parent = parent.parentNode) { + const parentContext = + (parent as any)[MONKEY_PATCH_KEY_NAME] as ElementContext | LViewData | null; + if (parentContext) { + const lViewData = + Array.isArray(parentContext) ? (parentContext as LViewData) : parentContext.lViewData; + const index = findMatchingElement(element, lViewData); + if (index >= 0) { + context = {index, native: element, lViewData}; + attachLViewDataToNode(element, context); + break; + } + } + } + } + return (context as ElementContext) || null; +} + +/** Locates the element within the given LViewData and returns the matching index */ +function findMatchingElement(element: RElement, lViewData: LViewData): number { + for (let i = HEADER_OFFSET; i < lViewData.length; i++) { + let result = lViewData[i]; + if (result) { + // special case for styling since when [class] and [style] bindings + // are used they will wrap the element into a StylingContext array + if (Array.isArray(result)) { + result = result[StylingIndex.ElementPosition]; + } + if (result.native === element) return i; + } + } + return -1; +} + +/** Assigns the given data to a DOM element using monkey-patching */ +export function attachLViewDataToNode(node: any, data: LViewData | ElementContext) { + node[MONKEY_PATCH_KEY_NAME] = data; +} diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 40c387ea08..32ac12d240 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -7,7 +7,7 @@ */ import {assertEqual, assertLessThan} from './assert'; -import {NO_CHANGE, _getViewData, adjustBlueprintForNewNode, bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, createLNode, getPreviousOrParentNode, getRenderer, load, resetApplicationState} from './instructions'; +import {NO_CHANGE, _getViewData, adjustBlueprintForNewNode, bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, createLNode, getPreviousOrParentNode, getRenderer, load, resetComponentState} from './instructions'; import {RENDER_PARENT} from './interfaces/container'; import {LContainerNode, LNode, TContainerNode, TElementNode, TNodeType} from './interfaces/node'; import {BINDING_INDEX, HEADER_OFFSET, TVIEW} from './interfaces/view'; @@ -305,7 +305,7 @@ export function i18nApply(startIndex: number, instructions: I18nInstruction[]): const renderer = getRenderer(); let localParentNode: LNode = getParentLNode(load(startIndex)) || getPreviousOrParentNode(); let localPreviousNode: LNode = localParentNode; - resetApplicationState(); // We don't want to add to the tree with the wrong previous node + resetComponentState(); // We don't want to add to the tree with the wrong previous node for (let i = 0; i < instructions.length; i++) { const instruction = instructions[i] as number; @@ -335,7 +335,7 @@ export function i18nApply(startIndex: number, instructions: I18nInstruction[]): const textLNode = createLNode(lastNodeIndex - HEADER_OFFSET, TNodeType.Element, textRNode, null, null); localPreviousNode = appendI18nNode(textLNode, localParentNode, localPreviousNode); - resetApplicationState(); + resetComponentState(); break; case I18nInstructions.CloseNode: localPreviousNode = localParentNode; diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 531c7bc3f8..d1da57dbde 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -13,6 +13,7 @@ import {Sanitizer} from '../sanitization/security'; import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual} from './assert'; +import {attachLViewDataToNode} from './element_discovery'; import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; @@ -102,6 +103,12 @@ export function getCurrentSanitizer(): Sanitizer|null { return viewData && viewData[SANITIZER]; } +/** + * Store the element depth count. This is used to identify the root elements of the template + * so that we can than attach `LViewData` to only those elements. + */ +let elementDepthCount !: number; + /** * Returns the current OpaqueViewState instance. * @@ -515,9 +522,10 @@ export function adjustBlueprintForNewNode(view: LViewData) { /** * Resets the application state. */ -export function resetApplicationState() { +export function resetComponentState() { isParent = false; previousOrParentNode = null !; + elementDepthCount = 0; } /** @@ -537,7 +545,7 @@ export function renderTemplate( directives?: DirectiveDefListOrFactory | null, pipes?: PipeDefListOrFactory | null, sanitizer?: Sanitizer | null): LElementNode { if (host == null) { - resetApplicationState(); + resetComponentState(); rendererFactory = providedRendererFactory; const tView = getOrCreateTView(templateFn, consts, vars, directives || null, pipes || null, null); @@ -796,6 +804,14 @@ export function elementStart( } appendChild(getParentLNode(node), native, viewData); createDirectivesAndLocals(node, localRefs); + + // any immediate children of a component or template container must be pre-emptively + // monkey-patched with the component view data so that the element can be inspected + // later on using any element discovery utility methods (see `element_discovery.ts`) + if (elementDepthCount === 0) { + attachLViewDataToNode(native, viewData); + } + elementDepthCount++; } /** @@ -1171,7 +1187,7 @@ export function locateHostElement( export function hostElement( tag: string, rNode: RElement | null, def: ComponentDefInternal, sanitizer?: Sanitizer | null): LElementNode { - resetApplicationState(); + resetComponentState(); const node = createLNode( 0, TNodeType.Element, rNode, null, null, createLViewData( @@ -1296,6 +1312,7 @@ export function elementEnd(): void { currentQueries && (currentQueries = currentQueries.addNode(previousOrParentNode)); queueLifecycleHooks(previousOrParentNode.tNode.flags, tView); currentElementNode = null; + elementDepthCount--; } /** @@ -2244,6 +2261,7 @@ export function projection(nodeIndex: number, selectorIndex: number = 0, attrs?: grandparent.data[RENDER_PARENT] ! : parent as LElementNode; + const parentView = viewData[HOST_NODE].view; while (nodeToProject) { if (nodeToProject.type === TNodeType.Projection) { // This node is re-projected, so we must go up the tree to get its projected nodes. @@ -2261,7 +2279,8 @@ export function projection(nodeIndex: number, selectorIndex: number = 0, attrs?: const lNode = projectedView[nodeToProject.index]; lNode.tNode.flags |= TNodeFlags.isProjected; appendProjectedNode( - lNode as LTextNode | LElementNode | LContainerNode, parent, viewData, renderParent); + lNode as LTextNode | LElementNode | LContainerNode, parent, viewData, renderParent, + parentView); } // If we are finished with a list of re-projected nodes, we need to get diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 25ee3abd9a..93aa7f4821 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -7,6 +7,7 @@ */ import {assertDefined} from './assert'; +import {attachLViewDataToNode} from './element_discovery'; import {callHooks} from './hooks'; import {LContainer, RENDER_PARENT, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; import {LContainerNode, LElementContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, TNode, TNodeFlags, TNodeType, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; @@ -694,8 +695,14 @@ export function removeChild(parent: LNode, child: RNode | null, currentView: LVi export function appendProjectedNode( node: LElementNode | LElementContainerNode | LTextNode | LContainerNode, currentParent: LElementNode | LElementContainerNode | LViewNode, currentView: LViewData, - renderParent: LElementNode): void { + renderParent: LElementNode, parentView: LViewData): void { appendChild(currentParent, node.native, currentView); + + // the projected contents are processed while in the shadow view (which is the currentView) + // therefore we need to extract the view where the host element lives since it's the + // logical container of the content projected views + attachLViewDataToNode(node.native, parentView); + if (node.tNode.type === TNodeType.Container) { // The node we are adding is a container and we are adding it to an element which // is not a component (no more re-projection). @@ -710,10 +717,11 @@ export function appendProjectedNode( } } else if (node.tNode.type === TNodeType.ElementContainer) { let ngContainerChild = getChildLNode(node as LElementContainerNode); + const parentView = currentView[HOST_NODE].view; while (ngContainerChild) { appendProjectedNode( ngContainerChild as LElementNode | LElementContainerNode | LTextNode | LContainerNode, - currentParent, currentView, renderParent); + currentParent, currentView, renderParent, parentView); ngContainerChild = getNextLNode(ngContainerChild); } } diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index f176c906bc..73264aa493 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -273,7 +273,7 @@ "name": "renderEmbeddedTemplate" }, { - "name": "resetApplicationState" + "name": "resetComponentState" }, { "name": "setHostBindings" diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 2f1cc03898..3529edde2a 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -74,6 +74,9 @@ { "name": "IterableDiffers" }, + { + "name": "MONKEY_PATCH_KEY_NAME" + }, { "name": "NEXT" }, @@ -326,6 +329,9 @@ { "name": "assertTemplate" }, + { + "name": "attachLViewDataToNode" + }, { "name": "baseDirectiveCreate" }, @@ -837,7 +843,7 @@ "name": "renderStyling" }, { - "name": "resetApplicationState" + "name": "resetComponentState" }, { "name": "resolveDirective" diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index ceb4d8a9df..f11c174565 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -422,6 +422,9 @@ { "name": "MODIFIER_KEY_GETTERS" }, + { + "name": "MONKEY_PATCH_KEY_NAME" + }, { "name": "MULTI_PROVIDER_FN" }, @@ -1157,6 +1160,9 @@ { "name": "assertTemplate" }, + { + "name": "attachLViewDataToNode" + }, { "name": "baseDirectiveCreate" }, @@ -2163,7 +2169,7 @@ "name": "renderStyling" }, { - "name": "resetApplicationState" + "name": "resetComponentState" }, { "name": "resolveDirective" diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 309362d546..a14a48af9e 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -21,6 +21,8 @@ import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; import {NgIf} from './common_with_def'; import {ComponentFixture, TemplateFixture, containerEl, createComponent, renderToHtml} from './render_util'; +import {MONKEY_PATCH_KEY_NAME, getElementContext} from '../../src/render3/element_discovery'; +import {StylingIndex} from '../../src/render3/styling'; describe('render3 integration test', () => { @@ -1404,7 +1406,6 @@ describe('render3 integration test', () => { } }); } - const rendererFactory = new MockRendererFactory(); new ComponentFixture(StyledComp, {rendererFactory}); expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']); @@ -1412,6 +1413,419 @@ describe('render3 integration test', () => { }); }); + describe('element discovery', () => { + it('should only monkey-patch immediate child nodes in a component', () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 2, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + elementStart(1, 'p'); + elementEnd(); + elementEnd(); + } + if (rf & RenderFlags.Update) { + } + } + }); + } + + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const host = fixture.hostElement; + const parent = host.querySelector('div') as any; + const child = host.querySelector('p') as any; + + expect(parent[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(child[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + }); + + it('should only monkey-patch immediate child nodes in a sub component', () => { + class ChildComp { + static ngComponentDef = defineComponent({ + type: ChildComp, + selectors: [['child-comp']], + factory: () => new ChildComp(), + consts: 3, + vars: 0, + template: (rf: RenderFlags, ctx: ChildComp) => { + if (rf & RenderFlags.Create) { + element(0, 'div'); + element(1, 'div'); + element(2, 'div'); + } + } + }); + } + + class ParentComp { + static ngComponentDef = defineComponent({ + type: ParentComp, + selectors: [['parent-comp']], + directives: [ChildComp], + factory: () => new ParentComp(), + consts: 2, + vars: 0, + template: (rf: RenderFlags, ctx: ParentComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + elementStart(1, 'child-comp'); + elementEnd(); + elementEnd(); + } + } + }); + } + + const fixture = new ComponentFixture(ParentComp); + fixture.update(); + + const host = fixture.hostElement; + const child = host.querySelector('child-comp') as any; + expect(child[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + + const [kid1, kid2, kid3] = Array.from(host.querySelectorAll('child-comp > *')); + expect(kid1[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(kid2[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(kid3[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + }); + + it('should only monkey-patch immediate child nodes in an embedded template container', () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + directives: [NgIf], + factory: () => new StructuredComp(), + consts: 2, + vars: 1, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + template(1, (rf, ctx) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + element(1, 'p'); + elementEnd(); + element(2, 'div'); + } + }, 3, 0, null, ['ngIf', '']); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementProperty(1, 'ngIf', true); + } + } + }); + } + + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const host = fixture.hostElement; + const [section, div1, p, div2] = Array.from(host.querySelectorAll('section, div, p')); + + expect(section.nodeName.toLowerCase()).toBe('section'); + expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + + expect(div1.nodeName.toLowerCase()).toBe('div'); + expect(div1[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + + expect(p.nodeName.toLowerCase()).toBe('p'); + expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + + expect(div2.nodeName.toLowerCase()).toBe('div'); + expect(div2[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + }); + + it('should return a context object from a given dom node', () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + directives: [NgIf], + factory: () => new StructuredComp(), + consts: 2, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + element(0, 'section'); + element(1, 'div'); + } + } + }); + } + + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const section = fixture.hostElement.querySelector('section') !; + const sectionContext = getElementContext(section) !; + const sectionLView = sectionContext.lViewData; + expect(sectionContext.index).toEqual(HEADER_OFFSET); + expect(sectionLView.length).toBeGreaterThan(HEADER_OFFSET); + expect(sectionContext.native).toBe(section); + + const div = fixture.hostElement.querySelector('div') !; + const divContext = getElementContext(div) !; + const divLView = divContext.lViewData; + expect(divContext.index).toEqual(HEADER_OFFSET + 1); + expect(divLView.length).toBeGreaterThan(HEADER_OFFSET); + expect(divContext.native).toBe(div); + + expect(divLView).toBe(sectionLView); + }); + + it('should cache the element context on a element was pre-emptively monkey-patched', () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + element(0, 'section'); + } + } + }); + } + + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const section = fixture.hostElement.querySelector('section') !as any; + const result1 = section[MONKEY_PATCH_KEY_NAME]; + expect(Array.isArray(result1)).toBeTruthy(); + + const context = getElementContext(section) !; + const result2 = section[MONKEY_PATCH_KEY_NAME]; + expect(Array.isArray(result2)).toBeFalsy(); + + expect(result2).toBe(context); + expect(result2.lViewData).toBe(result1); + }); + + it('should cache the element context on an intermediate element that isn\'t pre-emptively monkey-patched', + () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 2, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + element(1, 'p'); + elementEnd(); + } + } + }); + } + + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const section = fixture.hostElement.querySelector('section') !as any; + expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + + const p = fixture.hostElement.querySelector('p') !as any; + expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + + const pContext = getElementContext(p) !; + expect(pContext.native).toBe(p); + expect(p[MONKEY_PATCH_KEY_NAME]).toBe(pContext); + }); + + it('should be able to pull in element context data even if the element is decorated using styling', + () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + elementStyling(['class-foo']); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementStylingApply(0); + } + } + }); + } + + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const section = fixture.hostElement.querySelector('section') !as any; + const result1 = section[MONKEY_PATCH_KEY_NAME]; + expect(Array.isArray(result1)).toBeTruthy(); + + const elementResult = result1[HEADER_OFFSET]; // first element + expect(Array.isArray(elementResult)).toBeTruthy(); + expect(elementResult[StylingIndex.ElementPosition].native).toBe(section); + + const context = getElementContext(section) !; + const result2 = section[MONKEY_PATCH_KEY_NAME]; + expect(Array.isArray(result2)).toBeFalsy(); + + expect(context.native).toBe(section); + }); + + it('should monkey-patch immediate child nodes in a content-projected region with a reference to the parent component', + () => { + /* + +
+ + welcome +
+

+

this content is projected

+ this content is projected also +

+
+
+
+ */ + class ProjectorComp { + static ngComponentDef = defineComponent({ + type: ProjectorComp, + selectors: [['projector-comp']], + factory: () => new ProjectorComp(), + consts: 4, + vars: 0, + template: (rf: RenderFlags, ctx: ProjectorComp) => { + if (rf & RenderFlags.Create) { + projectionDef(); + text(0, 'welcome'); + elementStart(1, 'header'); + elementStart(2, 'h1'); + projection(3); + elementEnd(); + elementEnd(); + } + if (rf & RenderFlags.Update) { + } + } + }); + } + + class ParentComp { + static ngComponentDef = defineComponent({ + type: ParentComp, + selectors: [['parent-comp']], + directives: [ProjectorComp], + factory: () => new ParentComp(), + consts: 5, + vars: 0, + template: (rf: RenderFlags, ctx: ParentComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + elementStart(1, 'projector-comp'); + elementStart(2, 'p'); + text(3, 'this content is projected'); + elementEnd(); + text(4, 'this content is projected also'); + elementEnd(); + elementEnd(); + } + } + }); + } + + const fixture = new ComponentFixture(ParentComp); + fixture.update(); + + const host = fixture.hostElement; + const textNode = host.firstChild as any; + const section = host.querySelector('section') !as any; + const projectorComp = host.querySelector('projector-comp') !as any; + const header = host.querySelector('header') !as any; + const h1 = host.querySelector('h1') !as any; + const p = host.querySelector('p') !as any; + const pText = p.firstChild as any; + const projectedTextNode = p.nextSibling; + + expect(projectorComp.children).toContain(header); + expect(h1.children).toContain(p); + + expect(textNode[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(projectorComp[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + expect(header[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(h1[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + expect(p[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(pText[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + expect(projectedTextNode[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + + const parentContext = getElementContext(section) !; + const shadowContext = getElementContext(header) !; + const projectedContext = getElementContext(p) !; + + const parentComponentData = parentContext.lViewData; + const shadowComponentData = shadowContext.lViewData; + const projectedComponentData = projectedContext.lViewData; + + expect(projectedComponentData).toBe(parentComponentData); + expect(shadowComponentData).not.toBe(parentComponentData); + }); + + it('should return `null` when an element context is retrieved that isn\'t situated in Angular', + () => { + const elm1 = document.createElement('div'); + const context1 = getElementContext(elm1); + expect(context1).toBeFalsy(); + + const elm2 = document.createElement('div'); + document.body.appendChild(elm2); + const context2 = getElementContext(elm2); + expect(context2).toBeFalsy(); + }); + + it('should return `null` when an element context is retrieved that is a DOM node that was not created by Angular', + () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + element(0, 'section'); + } + } + }); + } + + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const section = fixture.hostElement.querySelector('section') !as any; + const manuallyCreatedElement = document.createElement('div'); + section.appendChild(manuallyCreatedElement); + + const context = getElementContext(manuallyCreatedElement); + expect(context).toBeFalsy(); + }); + }); + describe('sanitization', () => { it('should sanitize data using the provided sanitization interface', () => { class SanitizationComp {