diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 56ac6c3cbd..3a50e6558b 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -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. diff --git a/packages/core/src/render3/context_discovery.ts b/packages/core/src/render3/context_discovery.ts new file mode 100644 index 0000000000..c1c0c0b5f4 --- /dev/null +++ b/packages/core/src/render3/context_discovery.ts @@ -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; +} diff --git a/packages/core/src/render3/debug.ts b/packages/core/src/render3/debug.ts index aa0a29f10b..36302daedd 100644 --- a/packages/core/src/render3/debug.ts +++ b/packages/core/src/render3/debug.ts @@ -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); } } @@ -112,4 +113,4 @@ class Render3DebugContext implements DebugContext { // TODO(vicb): check previous implementation logError(console: Console, ...values: any[]): void { console.error(...values); } -} \ No newline at end of file +} diff --git a/packages/core/src/render3/element_discovery.ts b/packages/core/src/render3/element_discovery.ts deleted file mode 100644 index d43244bfc2..0000000000 --- a/packages/core/src/render3/element_discovery.ts +++ /dev/null @@ -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; -} diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index d74e15d715..dd878deb1c 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -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( '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(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(component: T): LElementNode { +export function _getComponentHostLElementNode( + 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; } diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 136bccf3f7..bf4edfc8f2 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -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 diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 93aa7f4821..e09dcbf77c 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -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 diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 73264aa493..d8940de31d 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -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" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index c724b938e9..d868a21e82 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -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" }, diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index 8462722295..702fa9f978 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -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" }, diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index b099640117..9fa3c376ad 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -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', () => { diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 3351140da8..b86e0a0b72 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -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(type: ComponentType, opts?: CreateComponen * @deprecated use `TemplateFixture` or `ComponentFixture` */ export function toHtml(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(/^
/, '') .replace(/^
/, '') .replace(/<\/div>$/, '') .replace(' style=""', '') .replace(//g, '') .replace(//g, ''); + } else { + return ''; } }