feat(ivy): allow combined context discovery for components, directives and elements (#25754)

PR Close #25754
This commit is contained in:
Matias Niemelä 2018-08-29 13:52:03 -07:00 committed by Igor Minar
parent d2dfd48be0
commit 62be8c2e2f
12 changed files with 789 additions and 142 deletions

View File

@ -162,9 +162,9 @@ export {
} from './sanitization/bypass';
export {
ElementContext as ɵElementContext,
getElementContext as ɵgetElementContext
} from './render3/element_discovery';
LContext as ɵLContext,
getContext as ɵgetContext
} from './render3/context_discovery';
// we reexport these symbols just so that they are retained during the dead code elimination
// performed by rollup while it's creating fesm files.

View File

@ -0,0 +1,396 @@
/**
* @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 './ng_dev_mode';
import {assertEqual} from './assert';
import {LElementNode, TNode, TNodeFlags} from './interfaces/node';
import {RElement} from './interfaces/renderer';
import {CONTEXT, DIRECTIVES, HEADER_OFFSET, LViewData, TVIEW} from './interfaces/view';
import {readElementValue} from './util';
/**
* This property will be monkey-patched on elements, components and directives
*/
export const MONKEY_PATCH_KEY_NAME = '__ngContext__';
/**
* The internal view context which is specific to a given DOM element, directive or
* component instance. Each value in here (besides the LViewData and element node details)
* can be present, null or undefined. If undefined then it implies the value has not been
* looked up yet, otherwise, if null, then a lookup was executed and nothing was found.
*
* Each value will get filled when the respective value is examined within the getContext
* function. The component, element and each directive instance will share the same instance
* of the context.
*/
export interface LContext {
/** The component\'s view data */
lViewData: LViewData;
/** The index instance of the LNode */
lNodeIndex: number;
/** The instance of the DOM node that is attached to the lNode */
native: RElement;
/** The instance of the Component node */
component: {}|null|undefined;
/** The list of indices for the active directives that exist on this element */
directiveIndices: number[]|null|undefined;
/** The list of active directives that exist on this element */
directives: Array<{}>|null|undefined;
}
/** Returns the matching `LContext` data for a given DOM node, directive or component instance.
*
* This function will examine the provided DOM element, component, or directive instance\'s
* monkey-patched property to derive the `LContext` data. Once called then the monkey-patched
* value will be that of the newly created `LContext`.
*
* If the monkey-patched value is the `LViewData` instance then the context value for that
* target will be created and the monkey-patch reference will be updated. Therefore when this
* function is called it may mutate the provided element\'s, component\'s or any of the associated
* directive\'s monkey-patch values.
*
* 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). If the monkey-patch value is not
* detected for a component/directive instance then it will throw an error (all components and
* directives should be automatically monkey-patched by ivy).
*/
export function getContext(target: any): LContext|null {
let mpValue = readPatchedData(target);
if (mpValue) {
// only when it's an array is it considered an LViewData instance
// ... otherwise it's an already constructed LContext instance
if (Array.isArray(mpValue)) {
const lViewData: LViewData = mpValue !;
let lNodeIndex: number;
let component: any = undefined;
let directiveIndices: number[]|null|undefined = undefined;
let directives: any[]|null|undefined = undefined;
if (isComponentInstance(target)) {
lNodeIndex = findViaComponent(lViewData, target);
if (lNodeIndex == -1) {
throw new Error('The provided component was not found in the application');
}
component = target;
} else if (isDirectiveInstance(target)) {
lNodeIndex = findViaDirective(lViewData, target);
if (lNodeIndex == -1) {
throw new Error('The provided directive was not found in the application');
}
directiveIndices = discoverDirectiveIndices(lViewData, lNodeIndex);
directives = directiveIndices ? discoverDirectives(lViewData, directiveIndices) : null;
} else {
lNodeIndex = findViaNativeElement(lViewData, target as RElement);
if (lNodeIndex == -1) {
return null;
}
}
// the goal is not to fill the entire context full of data because the lookups
// are expensive. Instead, only the target data (the element, compontent or
// directive details) are filled into the context. If called multiple times
// with different target values then the missing target data will be filled in.
const lNode = getLNodeFromViewData(lViewData, lNodeIndex) !;
const existingCtx = readPatchedData(lNode.native);
const context: LContext = (existingCtx && !Array.isArray(existingCtx)) ?
existingCtx :
createLContext(lViewData, lNodeIndex, lNode.native);
// only when the component has been discovered then update the monkey-patch
if (component && context.component === undefined) {
context.component = component;
attachPatchData(context.component, context);
}
// only when the directives have been discovered then update the monkey-patch
if (directives && directiveIndices && context.directives === undefined) {
context.directiveIndices = directiveIndices;
context.directives = directives;
for (let i = 0; i < directives.length; i++) {
attachPatchData(directives[i], context);
}
}
attachPatchData(context.native, context);
mpValue = context;
}
} else {
const rElement = target as RElement;
ngDevMode && assertDomElement(rElement);
// if the context is not found then we need to traverse upwards up the DOM
// to find the nearest element that has already been monkey patched with data
let parent = rElement as any;
while (parent = parent.parentNode) {
const parentContext = readPatchedData(parent);
if (parentContext) {
let lViewData: LViewData|null;
if (Array.isArray(parentContext)) {
lViewData = parentContext as LViewData;
} else {
lViewData = parentContext.lViewData;
}
// the edge of the app was also reached here through another means
// (maybe because the DOM was changed manually).
if (!lViewData) {
return null;
}
const index = findViaNativeElement(lViewData, rElement);
if (index >= 0) {
const lNode = getLNodeFromViewData(lViewData, index) !;
const context = createLContext(lViewData, index, lNode.native);
attachPatchData(lNode.native, context);
mpValue = context;
break;
}
}
}
}
return (mpValue as LContext) || null;
}
/**
* Creates an empty instance of a `LContext` context
*/
function createLContext(lViewData: LViewData, lNodeIndex: number, native: RElement): LContext {
return {
lViewData,
lNodeIndex,
native,
component: undefined,
directiveIndices: undefined,
directives: undefined,
};
}
/**
* A utility function for retrieving the matching lElementNode
* from a given DOM element, component or directive.
*/
export function getLElementNode(target: any): LElementNode|null {
const context = getContext(target);
return context ? getLNodeFromViewData(context.lViewData, context.lNodeIndex) : null;
}
export function getLElementFromRootComponent(componentInstance: {}): LElementNode|null {
// the host element for the root component is ALWAYS the first element
// in the lViewData array (which is where HEADER_OFFSET points to)
return getLElementFromComponent(componentInstance, HEADER_OFFSET);
}
/**
* A simplified lookup function for finding the LElementNode from a component instance.
*
* This function exists for tree-shaking purposes to avoid having to pull in everything
* that `getContext` has in the event that an Angular application doesn't need to have
* any programmatic access to an element's context (only change detection uses this function).
*/
export function getLElementFromComponent(
componentInstance: {}, expectedLNodeIndex?: number): LElementNode|null {
let lViewData = readPatchedData(componentInstance);
let lNode: LElementNode;
if (Array.isArray(lViewData)) {
expectedLNodeIndex = expectedLNodeIndex || findViaComponent(lViewData, componentInstance);
lNode = readElementValue(lViewData[expectedLNodeIndex]);
const context = createLContext(lViewData, expectedLNodeIndex, lNode.native);
context.component = componentInstance;
attachPatchData(componentInstance, context);
attachPatchData(context.native, context);
} else {
const context = lViewData as any as LContext;
lNode = readElementValue(context.lViewData[context.lNodeIndex]);
}
return lNode;
}
/**
* Assigns the given data to the given target (which could be a component,
* directive or DOM node instance) using monkey-patching.
*/
export function attachPatchData(target: any, data: LViewData | LContext) {
target[MONKEY_PATCH_KEY_NAME] = data;
}
/**
* Returns the monkey-patch value data present on the target (which could be
* a component, directive or a DOM node).
*/
export function readPatchedData(target: any): LViewData|LContext|null {
return target[MONKEY_PATCH_KEY_NAME];
}
export function isComponentInstance(instance: any): boolean {
return instance && instance.constructor && instance.constructor.ngComponentDef;
}
export function isDirectiveInstance(instance: any): boolean {
return instance && instance.constructor && instance.constructor.ngDirectiveDef;
}
/**
* Locates the element within the given LViewData and returns the matching index
*/
function findViaNativeElement(lViewData: LViewData, native: RElement): number {
let tNode = lViewData[TVIEW].firstChild;
while (tNode) {
const lNode = getLNodeFromViewData(lViewData, tNode.index) !;
if (lNode.native === native) {
return tNode.index;
}
tNode = traverseNextElement(tNode);
}
return -1;
}
/**
* Locates the next tNode (child, sibling or parent).
*/
function traverseNextElement(tNode: TNode): TNode|null {
if (tNode.child) {
return tNode.child;
} else if (tNode.next) {
return tNode.next;
} else if (tNode.parent) {
return tNode.parent.next || null;
}
return null;
}
/**
* Locates the component within the given LViewData and returns the matching index
*/
function findViaComponent(lViewData: LViewData, componentInstance: {}): number {
const componentIndices = lViewData[TVIEW].components;
if (componentIndices) {
for (let i = 0; i < componentIndices.length; i++) {
const elementComponentIndex = componentIndices[i];
const lNodeData = readElementValue(lViewData[elementComponentIndex] !).data !;
if (lNodeData[CONTEXT] === componentInstance) {
return elementComponentIndex;
}
}
} else {
const rootNode = lViewData[HEADER_OFFSET];
const rootComponent = rootNode.data[CONTEXT];
if (rootComponent === componentInstance) {
// we are dealing with the root element here therefore we know that the
// element is the very first element after the HEADER data in the lView
return HEADER_OFFSET;
}
}
return -1;
}
/**
* Locates the directive within the given LViewData and returns the matching index
*/
function findViaDirective(lViewData: LViewData, directiveInstance: {}): number {
// if a directive is monkey patched then it will (by default)
// have a reference to the LViewData of the current view. The
// element bound to the directive being search lives somewhere
// in the view data. By first checking to see if the instance
// is actually present we can narrow down to which lElementNode
// contains the instance of the directive and then return the index
const directivesAcrossView = lViewData[DIRECTIVES];
const directiveIndex =
directivesAcrossView ? directivesAcrossView.indexOf(directiveInstance) : -1;
if (directiveIndex >= 0) {
let tNode = lViewData[TVIEW].firstChild;
while (tNode) {
const lNode = getLNodeFromViewData(lViewData, tNode.index) !;
const directiveIndexStart = getDirectiveStartIndex(lNode);
const directiveIndexEnd = getDirectiveEndIndex(lNode, directiveIndexStart);
if (directiveIndex >= directiveIndexStart && directiveIndex < directiveIndexEnd) {
return tNode.index;
}
tNode = traverseNextElement(tNode);
}
}
return -1;
}
function assertDomElement(element: any) {
assertEqual(element.nodeType, 1, 'The provided value must be an instance of an HTMLElement');
}
/**
* Retruns the instance of the LElementNode at the given index in the LViewData.
*
* This function will also unwrap the inner value incase it's stuffed into an
* array (which is what happens when [style] and [class] bindings are present
* in the view instructions for the element being returned).
*/
function getLNodeFromViewData(lViewData: LViewData, lElementIndex: number): LElementNode|null {
const value = lViewData[lElementIndex];
return value ? readElementValue(value) : null;
}
/**
* Returns a collection of directive index values that are used on the element
* (which is referenced by the lNodeIndex)
*/
function discoverDirectiveIndices(lViewData: LViewData, lNodeIndex: number): number[]|null {
const directivesAcrossView = lViewData[DIRECTIVES];
const lNode = getLNodeFromViewData(lViewData, lNodeIndex);
if (lNode && directivesAcrossView && directivesAcrossView.length) {
// this check for tNode is to determine if the calue is a LEmementNode instance
const directiveIndexStart = getDirectiveStartIndex(lNode);
const directiveIndexEnd = getDirectiveEndIndex(lNode, directiveIndexStart);
const directiveIndices: number[] = [];
for (let i = directiveIndexStart; i < directiveIndexEnd; i++) {
// special case since the instance of the component (if it exists)
// is stored in the directives array.
if (i > directiveIndexStart ||
!isComponentInstance(directivesAcrossView[directiveIndexStart])) {
directiveIndices.push(i);
}
}
return directiveIndices.length ? directiveIndices : null;
}
return null;
}
function discoverDirectives(lViewData: LViewData, directiveIndices: number[]): number[]|null {
const directives: any[] = [];
const directiveInstances = lViewData[DIRECTIVES];
if (directiveInstances) {
for (let i = 0; i < directiveIndices.length; i++) {
const directiveIndex = directiveIndices[i];
const directive = directiveInstances[directiveIndex];
directives.push(directive);
}
}
return directives;
}
function getDirectiveStartIndex(lNode: LElementNode): number {
// the tNode instances store a flag value which then has a
// pointer which tells the starting index of where all the
// active directives are in the master directive array
return lNode.tNode.flags >> TNodeFlags.DirectiveStartingIndexShift;
}
function getDirectiveEndIndex(lNode: LElementNode, startIndex: number): number {
// The end value is also apart of the same flag
// (see `TNodeFlags` to see how the flag bit shifting
// values are used).
const count = lNode.tNode.flags & TNodeFlags.DirectiveCountMask;
return count ? (startIndex + count) : -1;
}

View File

@ -11,8 +11,9 @@ import {Renderer2, RendererType2} from '../render/api';
import {DebugContext} from '../view';
import {DebugRenderer2, DebugRendererFactory2} from '../view/services';
import {getLElementNode} from './context_discovery';
import * as di from './di';
import {NG_HOST_SYMBOL, _getViewData} from './instructions';
import {_getViewData} from './instructions';
import {LElementNode} from './interfaces/node';
import {CONTEXT, DIRECTIVES, LViewData, TVIEW} from './interfaces/view';
@ -85,7 +86,7 @@ class Render3DebugContext implements DebugContext {
const currentNode = this.view[this.nodeIndex];
for (let dirIndex = 0; dirIndex < directives.length; dirIndex++) {
const directive = directives[dirIndex];
if (directive[NG_HOST_SYMBOL] === currentNode) {
if (getLElementNode(directive) === currentNode) {
matchedDirectives.push(directive.constructor);
}
}

View File

@ -1,87 +0,0 @@
/**
* @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

@ -13,7 +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 {attachPatchData, getLElementFromComponent, getLElementFromRootComponent} from './context_discovery';
import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors';
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container';
@ -31,12 +31,6 @@ import {StylingContext, allocStylingContext, createStylingContextTemplate, rende
import {assertDataInRangeInternal, isDifferent, loadElementInternal, loadInternal, stringify} from './util';
import {ViewRef} from './view_ref';
/**
* Directive (D) sets a property on all component instances using this constant as a key and the
* component's host node (LElement) as the value. This is used in methods like detectChanges to
* facilitate jumping from an instance to the host node.
*/
export const NG_HOST_SYMBOL = '__ngHostLNode__';
/**
* A permanent marker promise which signifies that the current CD tree is
@ -471,7 +465,11 @@ export function createLNode(
if (previousTNode.dynamicContainerNode) previousTNode.dynamicContainerNode.next = tNode;
}
}
node.tNode = tData[adjustedIndex] as TNode;
if (!tView.firstChild && type === TNodeType.Element) {
tView.firstChild = node.tNode;
}
// Now link ourselves into the tree.
if (isParent) {
@ -806,7 +804,7 @@ export function elementStart(
// 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);
attachPatchData(native, viewData);
}
elementDepthCount++;
}
@ -1096,7 +1094,8 @@ export function createTView(
components: null,
directiveRegistry: typeof directives === 'function' ? directives() : directives,
pipeRegistry: typeof pipes === 'function' ? pipes() : pipes,
currentMatches: null
currentMatches: null,
firstChild: null,
};
}
@ -1760,8 +1759,7 @@ export function baseDirectiveCreate<T>(
'directives should be created before any bindings');
ngDevMode && assertPreviousIsParent();
Object.defineProperty(
directive, NG_HOST_SYMBOL, {enumerable: false, value: previousOrParentNode});
attachPatchData(directive, viewData);
if (directives == null) viewData[DIRECTIVES] = directives = [];
@ -2416,7 +2414,7 @@ export function tick<T>(component: T): void {
function tickRootContext(rootContext: RootContext) {
for (let i = 0; i < rootContext.components.length; i++) {
const rootComponent = rootContext.components[i];
const hostNode = _getComponentHostLElementNode(rootComponent);
const hostNode = _getComponentHostLElementNode(rootComponent, true);
ngDevMode && assertDefined(hostNode.data, 'Component host node should be attached to an LView');
renderComponentOrTemplate(hostNode, getRootView(rootComponent), rootComponent);
@ -2865,9 +2863,11 @@ function assertDataNext(index: number, arr?: any[]) {
arr.length, index, `index ${index} expected to be at the end of arr (length ${arr.length})`);
}
export function _getComponentHostLElementNode<T>(component: T): LElementNode {
export function _getComponentHostLElementNode<T>(
component: T, isRootComponent?: boolean): LElementNode {
ngDevMode && assertDefined(component, 'expecting component got null');
const lElementNode = (component as any)[NG_HOST_SYMBOL] as LElementNode;
const lElementNode = isRootComponent ? getLElementFromRootComponent(component) ! :
getLElementFromComponent(component) !;
ngDevMode && assertDefined(component, 'object is not a component');
return lElementNode;
}

View File

@ -314,6 +314,11 @@ export interface TView {
*/
childIndex: number;
/**
* A reference to the first child node located in the view.
*/
firstChild: TNode|null;
/**
* Selector matches for a node are temporarily cached on the TView so the
* DI system can eagerly instantiate directives on the same node if they are

View File

@ -7,7 +7,7 @@
*/
import {assertDefined} from './assert';
import {attachLViewDataToNode} from './element_discovery';
import {attachPatchData} from './context_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';
@ -701,7 +701,7 @@ export function appendProjectedNode(
// 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);
attachPatchData(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

View File

@ -44,15 +44,15 @@
{
"name": "INJECTOR$1"
},
{
"name": "MONKEY_PATCH_KEY_NAME"
},
{
"name": "NEXT"
},
{
"name": "NG_ELEMENT_ID"
},
{
"name": "NG_HOST_SYMBOL"
},
{
"name": "NG_PROJECT_AS_ATTR_NAME"
},
@ -98,6 +98,9 @@
{
"name": "appendChild"
},
{
"name": "attachPatchData"
},
{
"name": "baseDirectiveCreate"
},
@ -125,6 +128,9 @@
{
"name": "componentRefresh"
},
{
"name": "createLContext"
},
{
"name": "createLNode"
},
@ -185,12 +191,21 @@
{
"name": "extractPipeDef"
},
{
"name": "findViaComponent"
},
{
"name": "firstTemplatePass"
},
{
"name": "getChildLNode"
},
{
"name": "getLElementFromComponent"
},
{
"name": "getLElementFromRootComponent"
},
{
"name": "getLViewChild"
},
@ -251,6 +266,9 @@
{
"name": "readElementValue"
},
{
"name": "readPatchedData"
},
{
"name": "refreshChildComponents"
},

View File

@ -83,9 +83,6 @@
{
"name": "NG_ELEMENT_ID"
},
{
"name": "NG_HOST_SYMBOL"
},
{
"name": "NG_PROJECT_AS_ATTR_NAME"
},
@ -333,7 +330,7 @@
"name": "assertTemplate"
},
{
"name": "attachLViewDataToNode"
"name": "attachPatchData"
},
{
"name": "baseDirectiveCreate"
@ -401,6 +398,9 @@
{
"name": "createLContainer"
},
{
"name": "createLContext"
},
{
"name": "createLNode"
},
@ -530,6 +530,9 @@
{
"name": "findDirectiveMatches"
},
{
"name": "findViaComponent"
},
{
"name": "firstTemplatePass"
},
@ -560,6 +563,12 @@
{
"name": "getInitialValue"
},
{
"name": "getLElementFromComponent"
},
{
"name": "getLElementFromRootComponent"
},
{
"name": "getLViewChild"
},
@ -812,6 +821,9 @@
{
"name": "readElementValue"
},
{
"name": "readPatchedData"
},
{
"name": "reference"
},

View File

@ -464,9 +464,6 @@
{
"name": "NG_ELEMENT_ID"
},
{
"name": "NG_HOST_SYMBOL"
},
{
"name": "NG_PROJECT_AS_ATTR_NAME"
},
@ -1161,7 +1158,7 @@
"name": "assertTemplate"
},
{
"name": "attachLViewDataToNode"
"name": "attachPatchData"
},
{
"name": "baseDirectiveCreate"
@ -1274,6 +1271,9 @@
{
"name": "createLContainer"
},
{
"name": "createLContext"
},
{
"name": "createLNode"
},
@ -1475,6 +1475,9 @@
{
"name": "findLocaleData"
},
{
"name": "findViaComponent"
},
{
"name": "firstTemplatePass"
},
@ -1589,6 +1592,12 @@
{
"name": "getInitialValue"
},
{
"name": "getLElementFromComponent"
},
{
"name": "getLElementFromRootComponent"
},
{
"name": "getLViewChild"
},
@ -2129,6 +2138,9 @@
{
"name": "readElementValue"
},
{
"name": "readPatchedData"
},
{
"name": "recursivelyProcessProviders"
},

View File

@ -15,13 +15,13 @@ import {AttributeMarker, defineComponent, defineDirective, injectElementRef, inj
import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, element, elementAttribute, elementClassProp, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, listener, load, loadDirective, projection, projectionDef, text, textBinding, template} from '../../src/render3/instructions';
import {InitialStylingFlags} from '../../src/render3/interfaces/definition';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {HEADER_OFFSET} from '../../src/render3/interfaces/view';
import {HEADER_OFFSET, CONTEXT, DIRECTIVES} from '../../src/render3/interfaces/view';
import {sanitizeUrl} from '../../src/sanitization/sanitization';
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 {MONKEY_PATCH_KEY_NAME, getContext} from '../../src/render3/context_discovery';
import {StylingIndex} from '../../src/render3/styling';
describe('render3 integration test', () => {
@ -1595,16 +1595,16 @@ describe('render3 integration test', () => {
fixture.update();
const section = fixture.hostElement.querySelector('section') !;
const sectionContext = getElementContext(section) !;
const sectionLView = sectionContext.lViewData;
expect(sectionContext.index).toEqual(HEADER_OFFSET);
const sectionContext = getContext(section) !;
const sectionLView = sectionContext.lViewData !;
expect(sectionContext.lNodeIndex).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);
const divContext = getContext(div) !;
const divLView = divContext.lViewData !;
expect(divContext.lNodeIndex).toEqual(HEADER_OFFSET + 1);
expect(divLView.length).toBeGreaterThan(HEADER_OFFSET);
expect(divContext.native).toBe(div);
@ -1634,7 +1634,7 @@ describe('render3 integration test', () => {
const result1 = section[MONKEY_PATCH_KEY_NAME];
expect(Array.isArray(result1)).toBeTruthy();
const context = getElementContext(section) !;
const context = getContext(section) !;
const result2 = section[MONKEY_PATCH_KEY_NAME];
expect(Array.isArray(result2)).toBeFalsy();
@ -1670,7 +1670,7 @@ describe('render3 integration test', () => {
const p = fixture.hostElement.querySelector('p') !as any;
expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy();
const pContext = getElementContext(p) !;
const pContext = getContext(p) !;
expect(pContext.native).toBe(p);
expect(p[MONKEY_PATCH_KEY_NAME]).toBe(pContext);
});
@ -1708,7 +1708,7 @@ describe('render3 integration test', () => {
expect(Array.isArray(elementResult)).toBeTruthy();
expect(elementResult[StylingIndex.ElementPosition].native).toBe(section);
const context = getElementContext(section) !;
const context = getContext(section) !;
const result2 = section[MONKEY_PATCH_KEY_NAME];
expect(Array.isArray(result2)).toBeFalsy();
@ -1802,9 +1802,9 @@ describe('render3 integration test', () => {
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 parentContext = getContext(section) !;
const shadowContext = getContext(header) !;
const projectedContext = getContext(p) !;
const parentComponentData = parentContext.lViewData;
const shadowComponentData = shadowContext.lViewData;
@ -1817,12 +1817,12 @@ describe('render3 integration test', () => {
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);
const context1 = getContext(elm1);
expect(context1).toBeFalsy();
const elm2 = document.createElement('div');
document.body.appendChild(elm2);
const context2 = getElementContext(elm2);
const context2 = getContext(elm2);
expect(context2).toBeFalsy();
});
@ -1850,9 +1850,291 @@ describe('render3 integration test', () => {
const manuallyCreatedElement = document.createElement('div');
section.appendChild(manuallyCreatedElement);
const context = getElementContext(manuallyCreatedElement);
const context = getContext(manuallyCreatedElement);
expect(context).toBeFalsy();
});
it('should by default monkey-patch the bootstrap component with context details', () => {
class StructuredComp {
static ngComponentDef = defineComponent({
type: StructuredComp,
selectors: [['structured-comp']],
factory: () => new StructuredComp(),
consts: 0,
vars: 0,
template: (rf: RenderFlags, ctx: StructuredComp) => {}
});
}
const fixture = new ComponentFixture(StructuredComp);
fixture.update();
const hostElm = fixture.hostElement;
const component = fixture.component;
const componentContext = (component as any)[MONKEY_PATCH_KEY_NAME];
expect(Array.isArray(componentContext)).toBeFalsy();
const hostContext = (hostElm as any)[MONKEY_PATCH_KEY_NAME];
expect(hostContext).toBe(componentContext);
const context1 = getContext(hostElm) !;
expect(context1).toBe(hostContext);
expect(context1.native).toEqual(hostElm);
const context2 = getContext(component) !;
expect(context2).toBe(context1);
expect(context2).toBe(hostContext);
expect(context2.native).toEqual(hostElm);
});
it('should by default monkey-patch the directives with LViewData so that they can be examined',
() => {
let myDir1Instance: MyDir1|null = null;
let myDir2Instance: MyDir2|null = null;
let myDir3Instance: MyDir2|null = null;
class MyDir1 {
static ngDirectiveDef = defineDirective({
type: MyDir1,
selectors: [['', 'my-dir-1', '']],
factory: () => myDir1Instance = new MyDir1()
});
}
class MyDir2 {
static ngDirectiveDef = defineDirective({
type: MyDir2,
selectors: [['', 'my-dir-2', '']],
factory: () => myDir2Instance = new MyDir2()
});
}
class MyDir3 {
static ngDirectiveDef = defineDirective({
type: MyDir3,
selectors: [['', 'my-dir-3', '']],
factory: () => myDir3Instance = new MyDir2()
});
}
class StructuredComp {
static ngComponentDef = defineComponent({
type: StructuredComp,
selectors: [['structured-comp']],
directives: [MyDir1, MyDir2, MyDir3],
factory: () => new StructuredComp(),
consts: 2,
vars: 0,
template: (rf: RenderFlags, ctx: StructuredComp) => {
if (rf & RenderFlags.Create) {
element(0, 'div', ['my-dir-1', '', 'my-dir-2', '']);
element(1, 'div', ['my-dir-3']);
}
}
});
}
const fixture = new ComponentFixture(StructuredComp);
fixture.update();
const hostElm = fixture.hostElement;
const div1 = hostElm.querySelector('div:first-child') !as any;
const div2 = hostElm.querySelector('div:last-child') !as any;
const context = getContext(hostElm) !;
const elementNode = context.lViewData[context.lNodeIndex];
const elmData = elementNode.data !;
const dirs = elmData[DIRECTIVES];
expect(dirs).toContain(myDir1Instance);
expect(dirs).toContain(myDir2Instance);
expect(dirs).toContain(myDir3Instance);
expect(Array.isArray((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy();
expect(Array.isArray((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy();
expect(Array.isArray((myDir3Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy();
const d1Context = getContext(myDir1Instance) !;
const d2Context = getContext(myDir2Instance) !;
const d3Context = getContext(myDir3Instance) !;
expect(d1Context.lViewData).toEqual(elmData);
expect(d2Context.lViewData).toEqual(elmData);
expect(d3Context.lViewData).toEqual(elmData);
expect((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d1Context);
expect((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d2Context);
expect((myDir3Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d3Context);
expect(d1Context.lNodeIndex).toEqual(HEADER_OFFSET);
expect(d1Context.native).toBe(div1);
expect(d1Context.directives as any[]).toEqual([myDir1Instance, myDir2Instance]);
expect(d2Context.lNodeIndex).toEqual(HEADER_OFFSET);
expect(d2Context.native).toBe(div1);
expect(d2Context.directives as any[]).toEqual([myDir1Instance, myDir2Instance]);
expect(d3Context.lNodeIndex).toEqual(HEADER_OFFSET + 1);
expect(d3Context.native).toBe(div2);
expect(d3Context.directives as any[]).toEqual([myDir3Instance]);
});
it('should monkey-patch the exact same context instance of the DOM node, component and any directives on the same element',
() => {
let myDir1Instance: MyDir1|null = null;
let myDir2Instance: MyDir2|null = null;
let childComponentInstance: ChildComp|null = null;
class MyDir1 {
static ngDirectiveDef = defineDirective({
type: MyDir1,
selectors: [['', 'my-dir-1', '']],
factory: () => myDir1Instance = new MyDir1()
});
}
class MyDir2 {
static ngDirectiveDef = defineDirective({
type: MyDir2,
selectors: [['', 'my-dir-2', '']],
factory: () => myDir2Instance = new MyDir2()
});
}
class ChildComp {
static ngComponentDef = defineComponent({
type: ChildComp,
selectors: [['child-comp']],
factory: () => childComponentInstance = new ChildComp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: ChildComp) => {
if (rf & RenderFlags.Create) {
element(0, 'div');
}
}
});
}
class ParentComp {
static ngComponentDef = defineComponent({
type: ParentComp,
selectors: [['parent-comp']],
directives: [ChildComp, MyDir1, MyDir2],
factory: () => new ParentComp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: ParentComp) => {
if (rf & RenderFlags.Create) {
element(0, 'child-comp', ['my-dir-1', '', 'my-dir-2', '']);
}
}
});
}
const fixture = new ComponentFixture(ParentComp);
fixture.update();
const childCompHostElm = fixture.hostElement.querySelector('child-comp') !as any;
const lViewData = childCompHostElm[MONKEY_PATCH_KEY_NAME];
expect(Array.isArray(lViewData)).toBeTruthy();
expect((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lViewData);
expect((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lViewData);
expect((childComponentInstance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lViewData);
const childNodeContext = getContext(childCompHostElm) !;
expect(childNodeContext.component).toBeFalsy();
expect(childNodeContext.directives).toBeFalsy();
assertMonkeyPatchValueIsLViewData(myDir1Instance);
assertMonkeyPatchValueIsLViewData(myDir2Instance);
assertMonkeyPatchValueIsLViewData(childComponentInstance);
expect(getContext(myDir1Instance)).toBe(childNodeContext);
expect(childNodeContext.component).toBeFalsy();
expect(childNodeContext.directives !.length).toEqual(2);
assertMonkeyPatchValueIsLViewData(myDir1Instance, false);
assertMonkeyPatchValueIsLViewData(myDir2Instance, false);
assertMonkeyPatchValueIsLViewData(childComponentInstance);
expect(getContext(myDir2Instance)).toBe(childNodeContext);
expect(childNodeContext.component).toBeFalsy();
expect(childNodeContext.directives !.length).toEqual(2);
assertMonkeyPatchValueIsLViewData(myDir1Instance, false);
assertMonkeyPatchValueIsLViewData(myDir2Instance, false);
assertMonkeyPatchValueIsLViewData(childComponentInstance);
expect(getContext(childComponentInstance)).toBe(childNodeContext);
expect(childNodeContext.component).toBeTruthy();
expect(childNodeContext.directives !.length).toEqual(2);
assertMonkeyPatchValueIsLViewData(myDir1Instance, false);
assertMonkeyPatchValueIsLViewData(myDir2Instance, false);
assertMonkeyPatchValueIsLViewData(childComponentInstance, false);
function assertMonkeyPatchValueIsLViewData(value: any, yesOrNo = true) {
expect(Array.isArray((value as any)[MONKEY_PATCH_KEY_NAME])).toBe(yesOrNo);
}
});
it('should monkey-patch sub components with the view data and then replace them with the context result once a lookup occurs',
() => {
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 context = getContext(child) !;
expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
const componentData = context.lViewData[context.lNodeIndex].data;
const component = componentData[CONTEXT];
expect(component instanceof ChildComp).toBeTruthy();
expect(component[MONKEY_PATCH_KEY_NAME]).toBe(context.lViewData);
const componentContext = getContext(component) !;
expect(component[MONKEY_PATCH_KEY_NAME]).toBe(componentContext);
expect(componentContext.lNodeIndex).toEqual(context.lNodeIndex);
expect(componentContext.native).toEqual(context.native);
expect(componentContext.lViewData).toEqual(context.lViewData);
});
});
describe('sanitization', () => {

View File

@ -10,9 +10,10 @@ import {stringifyElement} from '@angular/platform-browser/testing/src/browser_ut
import {Injector} from '../../src/di/injector';
import {CreateComponentOptions} from '../../src/render3/component';
import {getContext, isComponentInstance} from '../../src/render3/context_discovery';
import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition';
import {ComponentTemplate, ComponentType, DirectiveDefInternal, DirectiveType, PublicFeature, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index';
import {NG_HOST_SYMBOL, renderTemplate} from '../../src/render3/instructions';
import {renderTemplate} from '../../src/render3/instructions';
import {DirectiveDefList, DirectiveTypesOrFactory, PipeDefInternal, PipeDefList, PipeTypesOrFactory} from '../../src/render3/interfaces/definition';
import {LElementNode} from '../../src/render3/interfaces/node';
import {RElement, RText, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
@ -219,17 +220,24 @@ export function renderComponent<T>(type: ComponentType<T>, opts?: CreateComponen
* @deprecated use `TemplateFixture` or `ComponentFixture`
*/
export function toHtml<T>(componentOrElement: T | RElement): string {
const node = (componentOrElement as any)[NG_HOST_SYMBOL] as LElementNode;
if (node) {
return toHtml(node.native);
let element: any;
if (isComponentInstance(componentOrElement)) {
const context = getContext(componentOrElement);
element = context ? context.native : null;
} else {
return stringifyElement(componentOrElement)
element = componentOrElement;
}
if (element) {
return stringifyElement(element)
.replace(/^<div host="">/, '')
.replace(/^<div fixture="mark">/, '')
.replace(/<\/div>$/, '')
.replace(' style=""', '')
.replace(/<!--container-->/g, '')
.replace(/<!--ng-container-->/g, '');
} else {
return '';
}
}