feat(ivy): add support for resolving view data from a DOM node (#25627)
PR Close #25627
This commit is contained in:
87
packages/core/src/render3/element_discovery.ts
Normal file
87
packages/core/src/render3/element_discovery.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user