feat(ivy): add support for resolving view data from a DOM node (#25627)
PR Close #25627
This commit is contained in:
parent
317d40d879
commit
0024d68add
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<T>(
|
|||
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<any>,
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -273,7 +273,7 @@
|
|||
"name": "renderEmbeddedTemplate"
|
||||
},
|
||||
{
|
||||
"name": "resetApplicationState"
|
||||
"name": "resetComponentState"
|
||||
},
|
||||
{
|
||||
"name": "setHostBindings"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
() => {
|
||||
/*
|
||||
<!-- DOM view -->
|
||||
<section>
|
||||
<projection-comp>
|
||||
welcome
|
||||
<header>
|
||||
<h1>
|
||||
<p>this content is projected</p>
|
||||
this content is projected also
|
||||
</h1>
|
||||
</header>
|
||||
</projection-comp>
|
||||
</section>
|
||||
*/
|
||||
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 {
|
||||
|
|
Loading…
Reference in New Issue