feat(ivy): add support for resolving view data from a DOM node (#25627)

PR Close #25627
This commit is contained in:
Matias Niemelä
2018-08-22 16:57:40 -07:00
parent 317d40d879
commit 0024d68add
9 changed files with 558 additions and 13 deletions

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

View File

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

View File

@ -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

View File

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