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,
|
bypassSanitizationTrustUrl as ɵbypassSanitizationTrustUrl,
|
||||||
bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl,
|
bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl,
|
||||||
} from './sanitization/bypass';
|
} from './sanitization/bypass';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ElementContext as ɵElementContext,
|
||||||
|
getElementContext as ɵgetElementContext
|
||||||
|
} from './render3/element_discovery';
|
||||||
// clang-format on
|
// 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 {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 {RENDER_PARENT} from './interfaces/container';
|
||||||
import {LContainerNode, LNode, TContainerNode, TElementNode, TNodeType} from './interfaces/node';
|
import {LContainerNode, LNode, TContainerNode, TElementNode, TNodeType} from './interfaces/node';
|
||||||
import {BINDING_INDEX, HEADER_OFFSET, TVIEW} from './interfaces/view';
|
import {BINDING_INDEX, HEADER_OFFSET, TVIEW} from './interfaces/view';
|
||||||
|
@ -305,7 +305,7 @@ export function i18nApply(startIndex: number, instructions: I18nInstruction[]):
|
||||||
const renderer = getRenderer();
|
const renderer = getRenderer();
|
||||||
let localParentNode: LNode = getParentLNode(load(startIndex)) || getPreviousOrParentNode();
|
let localParentNode: LNode = getParentLNode(load(startIndex)) || getPreviousOrParentNode();
|
||||||
let localPreviousNode: LNode = localParentNode;
|
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++) {
|
for (let i = 0; i < instructions.length; i++) {
|
||||||
const instruction = instructions[i] as number;
|
const instruction = instructions[i] as number;
|
||||||
|
@ -335,7 +335,7 @@ export function i18nApply(startIndex: number, instructions: I18nInstruction[]):
|
||||||
const textLNode =
|
const textLNode =
|
||||||
createLNode(lastNodeIndex - HEADER_OFFSET, TNodeType.Element, textRNode, null, null);
|
createLNode(lastNodeIndex - HEADER_OFFSET, TNodeType.Element, textRNode, null, null);
|
||||||
localPreviousNode = appendI18nNode(textLNode, localParentNode, localPreviousNode);
|
localPreviousNode = appendI18nNode(textLNode, localParentNode, localPreviousNode);
|
||||||
resetApplicationState();
|
resetComponentState();
|
||||||
break;
|
break;
|
||||||
case I18nInstructions.CloseNode:
|
case I18nInstructions.CloseNode:
|
||||||
localPreviousNode = localParentNode;
|
localPreviousNode = localParentNode;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {Sanitizer} from '../sanitization/security';
|
||||||
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
|
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
|
||||||
|
|
||||||
import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual} from './assert';
|
import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual} from './assert';
|
||||||
|
import {attachLViewDataToNode} from './element_discovery';
|
||||||
import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors';
|
import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors';
|
||||||
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
|
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
|
||||||
import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container';
|
import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container';
|
||||||
|
@ -102,6 +103,12 @@ export function getCurrentSanitizer(): Sanitizer|null {
|
||||||
return viewData && viewData[SANITIZER];
|
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.
|
* Returns the current OpaqueViewState instance.
|
||||||
*
|
*
|
||||||
|
@ -515,9 +522,10 @@ export function adjustBlueprintForNewNode(view: LViewData) {
|
||||||
/**
|
/**
|
||||||
* Resets the application state.
|
* Resets the application state.
|
||||||
*/
|
*/
|
||||||
export function resetApplicationState() {
|
export function resetComponentState() {
|
||||||
isParent = false;
|
isParent = false;
|
||||||
previousOrParentNode = null !;
|
previousOrParentNode = null !;
|
||||||
|
elementDepthCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -537,7 +545,7 @@ export function renderTemplate<T>(
|
||||||
directives?: DirectiveDefListOrFactory | null, pipes?: PipeDefListOrFactory | null,
|
directives?: DirectiveDefListOrFactory | null, pipes?: PipeDefListOrFactory | null,
|
||||||
sanitizer?: Sanitizer | null): LElementNode {
|
sanitizer?: Sanitizer | null): LElementNode {
|
||||||
if (host == null) {
|
if (host == null) {
|
||||||
resetApplicationState();
|
resetComponentState();
|
||||||
rendererFactory = providedRendererFactory;
|
rendererFactory = providedRendererFactory;
|
||||||
const tView =
|
const tView =
|
||||||
getOrCreateTView(templateFn, consts, vars, directives || null, pipes || null, null);
|
getOrCreateTView(templateFn, consts, vars, directives || null, pipes || null, null);
|
||||||
|
@ -796,6 +804,14 @@ export function elementStart(
|
||||||
}
|
}
|
||||||
appendChild(getParentLNode(node), native, viewData);
|
appendChild(getParentLNode(node), native, viewData);
|
||||||
createDirectivesAndLocals(node, localRefs);
|
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(
|
export function hostElement(
|
||||||
tag: string, rNode: RElement | null, def: ComponentDefInternal<any>,
|
tag: string, rNode: RElement | null, def: ComponentDefInternal<any>,
|
||||||
sanitizer?: Sanitizer | null): LElementNode {
|
sanitizer?: Sanitizer | null): LElementNode {
|
||||||
resetApplicationState();
|
resetComponentState();
|
||||||
const node = createLNode(
|
const node = createLNode(
|
||||||
0, TNodeType.Element, rNode, null, null,
|
0, TNodeType.Element, rNode, null, null,
|
||||||
createLViewData(
|
createLViewData(
|
||||||
|
@ -1296,6 +1312,7 @@ export function elementEnd(): void {
|
||||||
currentQueries && (currentQueries = currentQueries.addNode(previousOrParentNode));
|
currentQueries && (currentQueries = currentQueries.addNode(previousOrParentNode));
|
||||||
queueLifecycleHooks(previousOrParentNode.tNode.flags, tView);
|
queueLifecycleHooks(previousOrParentNode.tNode.flags, tView);
|
||||||
currentElementNode = null;
|
currentElementNode = null;
|
||||||
|
elementDepthCount--;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2244,6 +2261,7 @@ export function projection(nodeIndex: number, selectorIndex: number = 0, attrs?:
|
||||||
grandparent.data[RENDER_PARENT] ! :
|
grandparent.data[RENDER_PARENT] ! :
|
||||||
parent as LElementNode;
|
parent as LElementNode;
|
||||||
|
|
||||||
|
const parentView = viewData[HOST_NODE].view;
|
||||||
while (nodeToProject) {
|
while (nodeToProject) {
|
||||||
if (nodeToProject.type === TNodeType.Projection) {
|
if (nodeToProject.type === TNodeType.Projection) {
|
||||||
// This node is re-projected, so we must go up the tree to get its projected nodes.
|
// 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];
|
const lNode = projectedView[nodeToProject.index];
|
||||||
lNode.tNode.flags |= TNodeFlags.isProjected;
|
lNode.tNode.flags |= TNodeFlags.isProjected;
|
||||||
appendProjectedNode(
|
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
|
// If we are finished with a list of re-projected nodes, we need to get
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {assertDefined} from './assert';
|
import {assertDefined} from './assert';
|
||||||
|
import {attachLViewDataToNode} from './element_discovery';
|
||||||
import {callHooks} from './hooks';
|
import {callHooks} from './hooks';
|
||||||
import {LContainer, RENDER_PARENT, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container';
|
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';
|
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(
|
export function appendProjectedNode(
|
||||||
node: LElementNode | LElementContainerNode | LTextNode | LContainerNode,
|
node: LElementNode | LElementContainerNode | LTextNode | LContainerNode,
|
||||||
currentParent: LElementNode | LElementContainerNode | LViewNode, currentView: LViewData,
|
currentParent: LElementNode | LElementContainerNode | LViewNode, currentView: LViewData,
|
||||||
renderParent: LElementNode): void {
|
renderParent: LElementNode, parentView: LViewData): void {
|
||||||
appendChild(currentParent, node.native, currentView);
|
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) {
|
if (node.tNode.type === TNodeType.Container) {
|
||||||
// The node we are adding is a container and we are adding it to an element which
|
// 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).
|
// is not a component (no more re-projection).
|
||||||
|
@ -710,10 +717,11 @@ export function appendProjectedNode(
|
||||||
}
|
}
|
||||||
} else if (node.tNode.type === TNodeType.ElementContainer) {
|
} else if (node.tNode.type === TNodeType.ElementContainer) {
|
||||||
let ngContainerChild = getChildLNode(node as LElementContainerNode);
|
let ngContainerChild = getChildLNode(node as LElementContainerNode);
|
||||||
|
const parentView = currentView[HOST_NODE].view;
|
||||||
while (ngContainerChild) {
|
while (ngContainerChild) {
|
||||||
appendProjectedNode(
|
appendProjectedNode(
|
||||||
ngContainerChild as LElementNode | LElementContainerNode | LTextNode | LContainerNode,
|
ngContainerChild as LElementNode | LElementContainerNode | LTextNode | LContainerNode,
|
||||||
currentParent, currentView, renderParent);
|
currentParent, currentView, renderParent, parentView);
|
||||||
ngContainerChild = getNextLNode(ngContainerChild);
|
ngContainerChild = getNextLNode(ngContainerChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,7 +273,7 @@
|
||||||
"name": "renderEmbeddedTemplate"
|
"name": "renderEmbeddedTemplate"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "resetApplicationState"
|
"name": "resetComponentState"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "setHostBindings"
|
"name": "setHostBindings"
|
||||||
|
|
|
@ -74,6 +74,9 @@
|
||||||
{
|
{
|
||||||
"name": "IterableDiffers"
|
"name": "IterableDiffers"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "MONKEY_PATCH_KEY_NAME"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "NEXT"
|
"name": "NEXT"
|
||||||
},
|
},
|
||||||
|
@ -326,6 +329,9 @@
|
||||||
{
|
{
|
||||||
"name": "assertTemplate"
|
"name": "assertTemplate"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "attachLViewDataToNode"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "baseDirectiveCreate"
|
"name": "baseDirectiveCreate"
|
||||||
},
|
},
|
||||||
|
@ -837,7 +843,7 @@
|
||||||
"name": "renderStyling"
|
"name": "renderStyling"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "resetApplicationState"
|
"name": "resetComponentState"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "resolveDirective"
|
"name": "resolveDirective"
|
||||||
|
|
|
@ -422,6 +422,9 @@
|
||||||
{
|
{
|
||||||
"name": "MODIFIER_KEY_GETTERS"
|
"name": "MODIFIER_KEY_GETTERS"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "MONKEY_PATCH_KEY_NAME"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "MULTI_PROVIDER_FN"
|
"name": "MULTI_PROVIDER_FN"
|
||||||
},
|
},
|
||||||
|
@ -1157,6 +1160,9 @@
|
||||||
{
|
{
|
||||||
"name": "assertTemplate"
|
"name": "assertTemplate"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "attachLViewDataToNode"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "baseDirectiveCreate"
|
"name": "baseDirectiveCreate"
|
||||||
},
|
},
|
||||||
|
@ -2163,7 +2169,7 @@
|
||||||
"name": "renderStyling"
|
"name": "renderStyling"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "resetApplicationState"
|
"name": "resetComponentState"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "resolveDirective"
|
"name": "resolveDirective"
|
||||||
|
|
|
@ -21,6 +21,8 @@ import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
|
||||||
|
|
||||||
import {NgIf} from './common_with_def';
|
import {NgIf} from './common_with_def';
|
||||||
import {ComponentFixture, TemplateFixture, containerEl, createComponent, renderToHtml} from './render_util';
|
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', () => {
|
describe('render3 integration test', () => {
|
||||||
|
|
||||||
|
@ -1404,7 +1406,6 @@ describe('render3 integration test', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const rendererFactory = new MockRendererFactory();
|
const rendererFactory = new MockRendererFactory();
|
||||||
new ComponentFixture(StyledComp, {rendererFactory});
|
new ComponentFixture(StyledComp, {rendererFactory});
|
||||||
expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']);
|
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', () => {
|
describe('sanitization', () => {
|
||||||
it('should sanitize data using the provided sanitization interface', () => {
|
it('should sanitize data using the provided sanitization interface', () => {
|
||||||
class SanitizationComp {
|
class SanitizationComp {
|
||||||
|
|
Loading…
Reference in New Issue