feat(ivy): support property bindings and interpolations in DebugElement (#28355)

DebugElement.properties should contain a map of element
property names to element property values, with entries
for both normal property bindings and host bindings.

This commit adds support for property bindings in
DebugElement.properties (including interpolations).

PR Close #28355
This commit is contained in:
Kara Erickson 2019-01-24 08:53:00 -08:00 committed by Jason Aden
parent 46aec4a58f
commit bf97d3b73e
7 changed files with 331 additions and 48 deletions

View File

@ -10,9 +10,10 @@ import {Injector} from '../di';
import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, isBrowserEvents, loadLContext, loadLContextFromNode} from '../render3/discovery_utils'; import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, isBrowserEvents, loadLContext, loadLContextFromNode} from '../render3/discovery_utils';
import {TNode} from '../render3/interfaces/node'; import {TNode} from '../render3/interfaces/node';
import {StylingIndex} from '../render3/interfaces/styling'; import {StylingIndex} from '../render3/interfaces/styling';
import {TVIEW} from '../render3/interfaces/view'; import {LView, TData, TVIEW} from '../render3/interfaces/view';
import {getProp, getValue, isClassBasedValue} from '../render3/styling/class_and_style_bindings'; import {getProp, getValue, isClassBasedValue} from '../render3/styling/class_and_style_bindings';
import {getStylingContext} from '../render3/styling/util'; import {getStylingContext} from '../render3/styling/util';
import {INTERPOLATION_DELIMITER, isPropMetadataString, renderStringify} from '../render3/util';
import {assertDomNode} from '../util/assert'; import {assertDomNode} from '../util/assert';
import {DebugContext} from '../view/index'; import {DebugContext} from '../view/index';
@ -240,14 +241,16 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
get name(): string { return this.nativeElement !.nodeName; } get name(): string { return this.nativeElement !.nodeName; }
/** /**
* Returns a map of property names to property values for an element. * Gets a map of property names to property values for an element.
* *
* This map includes: * This map includes:
* - Regular property bindings (e.g. `[id]="id"`) - TODO * - Regular property bindings (e.g. `[id]="id"`)
* - Host property bindings (e.g. `host: { '[id]': "id" }`) * - Host property bindings (e.g. `host: { '[id]': "id" }`)
* - Interpolated property bindings (e.g. `id="{{ value }}") - TODO * - Interpolated property bindings (e.g. `id="{{ value }}")
* *
* It should NOT include input property bindings or attribute bindings. * It does not include:
* - input property bindings (e.g. `[myCustomInput]="value"`)
* - attribute bindings (e.g. `[attr.role]="menu"`)
*/ */
get properties(): {[key: string]: any;} { get properties(): {[key: string]: any;} {
const context = loadLContext(this.nativeNode) !; const context = loadLContext(this.nativeNode) !;
@ -255,20 +258,9 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
const tData = lView[TVIEW].data; const tData = lView[TVIEW].data;
const tNode = tData[context.nodeIndex] as TNode; const tNode = tData[context.nodeIndex] as TNode;
// TODO(kara): include regular property binding values (not just host properties) const properties = collectPropertyBindings(tNode, lView, tData);
const properties: {[key: string]: string} = {}; const hostProperties = collectHostPropertyBindings(tNode, lView, tData);
return {...properties, ...hostProperties};
// Host binding values for a node are stored after directives on that node
let index = tNode.directiveEnd;
let propertyName = tData[index];
// When we reach a value in TView.data that is not a string, we know we've
// hit the next node's providers and directives and should stop copying data.
while (typeof propertyName === 'string') {
properties[propertyName] = lView[index];
propertyName = tData[++index];
}
return properties;
} }
get attributes(): {[key: string]: string | null;} { get attributes(): {[key: string]: string | null;} {
@ -410,6 +402,89 @@ function _queryNodeChildrenR3(
} }
} }
/**
* Iterates through the property bindings for a given node and generates
* a map of property names to values. This map only contains property bindings
* defined in templates, not in host bindings.
*/
function collectPropertyBindings(
tNode: TNode, lView: LView, tData: TData): {[key: string]: string} {
const properties: {[key: string]: string} = {};
let bindingIndex = getFirstBindingIndex(tNode.propertyMetadataStartIndex, tData);
while (bindingIndex < tNode.propertyMetadataEndIndex) {
let value = '';
let propMetadata = tData[bindingIndex] as string;
while (!isPropMetadataString(propMetadata)) {
// This is the first value for an interpolation. We need to build up
// the full interpolation by combining runtime values in LView with
// the static interstitial values stored in TData.
value += renderStringify(lView[bindingIndex]) + tData[bindingIndex];
propMetadata = tData[++bindingIndex] as string;
}
value += lView[bindingIndex];
// Property metadata string has 3 parts: property name, prefix, and suffix
const metadataParts = propMetadata.split(INTERPOLATION_DELIMITER);
const propertyName = metadataParts[0];
// Attr bindings don't have property names and should be skipped
if (propertyName) {
// Wrap value with prefix and suffix (will be '' for normal bindings)
properties[propertyName] = metadataParts[1] + value + metadataParts[2];
}
bindingIndex++;
}
return properties;
}
/**
* Retrieves the first binding index that holds values for this property
* binding.
*
* For normal bindings (e.g. `[id]="id"`), the binding index is the
* same as the metadata index. For interpolations (e.g. `id="{{id}}-{{name}}"`),
* there can be multiple binding values, so we might have to loop backwards
* from the metadata index until we find the first one.
*
* @param metadataIndex The index of the first property metadata string for
* this node.
* @param tData The data array for the current TView
* @returns The first binding index for this binding
*/
function getFirstBindingIndex(metadataIndex: number, tData: TData): number {
let currentBindingIndex = metadataIndex - 1;
// If the slot before the metadata holds a string, we know that this
// metadata applies to an interpolation with at least 2 bindings, and
// we need to search further to access the first binding value.
let currentValue = tData[currentBindingIndex];
// We need to iterate until we hit either a:
// - TNode (it is an element slot marking the end of `consts` section), OR a
// - metadata string (slot is attribute metadata or a previous node's property metadata)
while (typeof currentValue === 'string' && !isPropMetadataString(currentValue)) {
currentValue = tData[--currentBindingIndex];
}
return currentBindingIndex + 1;
}
function collectHostPropertyBindings(
tNode: TNode, lView: LView, tData: TData): {[key: string]: string} {
const properties: {[key: string]: string} = {};
// Host binding values for a node are stored after directives on that node
let hostPropIndex = tNode.directiveEnd;
let propMetadata = tData[hostPropIndex] as any;
// When we reach a value in TView.data that is not a string, we know we've
// hit the next node's providers and directives and should stop copying data.
while (typeof propMetadata === 'string') {
const propertyName = propMetadata.split(INTERPOLATION_DELIMITER)[0];
properties[propertyName] = lView[hostPropIndex];
propMetadata = tData[++hostPropIndex];
}
return properties;
}
// Need to keep the nodes in a global Map so that multiple angular apps are supported. // Need to keep the nodes in a global Map so that multiple angular apps are supported.
const _nativeNodeToDebugNode = new Map<any, DebugNode>(); const _nativeNodeToDebugNode = new Map<any, DebugNode>();

View File

@ -32,7 +32,7 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'
import {LQueries} from './interfaces/query'; import {LQueries} from './interfaces/query';
import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer'; import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization'; import {SanitizerFn} from './interfaces/sanitization';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view'; import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation'; import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
@ -41,7 +41,7 @@ import {getInitialClassNameValue, initializeStaticContext as initializeStaticSty
import {BoundPlayerFactory} from './styling/player_factory'; import {BoundPlayerFactory} from './styling/player_factory';
import {createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util'; import {createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util';
import {NO_CHANGE} from './tokens'; import {NO_CHANGE} from './tokens';
import {findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util'; import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util';
@ -719,8 +719,8 @@ export function createTView(
template: templateFn, template: templateFn,
viewQuery: viewQuery, viewQuery: viewQuery,
node: null !, node: null !,
data: blueprint.slice(), // Fill in to match HEADER_OFFSET in LView data: blueprint.slice().fill(null, bindingStartIndex),
childIndex: -1, // Children set in addToViewTree(), if any childIndex: -1, // Children set in addToViewTree(), if any
bindingStartIndex: bindingStartIndex, bindingStartIndex: bindingStartIndex,
viewQueryStartIndex: initialViewLength, viewQueryStartIndex: initialViewLength,
expandoStartIndex: initialViewLength, expandoStartIndex: initialViewLength,
@ -1154,14 +1154,9 @@ function elementPropertyInternal<T>(
validateProperty(propName); validateProperty(propName);
ngDevMode.rendererSetProperty++; ngDevMode.rendererSetProperty++;
} }
const tView = lView[TVIEW];
const lastBindingIndex = lView[BINDING_INDEX] - 1; savePropertyDebugData(tNode, lView, propName, lView[TVIEW].data, nativeOnly);
if (nativeOnly && tView.data[lastBindingIndex] == null) {
// We need to store the host property name so it can be accessed by DebugElement.properties.
// Host properties cannot have interpolations, so using the last binding index is
// sufficient.
tView.data[lastBindingIndex] = propName;
}
const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER]; const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER];
// It is assumed that the sanitizer is only added when the compiler determines that the property // It is assumed that the sanitizer is only added when the compiler determines that the property
// is risky, so sanitization can be done without further checks. // is risky, so sanitization can be done without further checks.
@ -1175,6 +1170,34 @@ function elementPropertyInternal<T>(
} }
} }
/**
* Stores debugging data for this property binding on first template pass.
* This enables features like DebugElement.properties.
*/
function savePropertyDebugData(
tNode: TNode, lView: LView, propName: string, tData: TData,
nativeOnly: boolean | undefined): void {
const lastBindingIndex = lView[BINDING_INDEX] - 1;
// Bind/interpolation functions save binding metadata in the last binding index,
// but leave the property name blank. If the interpolation delimiter is at the 0
// index, we know that this is our first pass and the property name still needs to
// be set.
const bindingMetadata = tData[lastBindingIndex] as string;
if (bindingMetadata[0] == INTERPOLATION_DELIMITER) {
tData[lastBindingIndex] = propName + bindingMetadata;
// We don't want to store indices for host bindings because they are stored in a
// different part of LView (the expando section).
if (!nativeOnly) {
if (tNode.propertyMetadataStartIndex == -1) {
tNode.propertyMetadataStartIndex = lastBindingIndex;
}
tNode.propertyMetadataEndIndex = lastBindingIndex + 1;
}
}
}
/** /**
* Constructs a TNode object from the arguments. * Constructs a TNode object from the arguments.
* *
@ -1204,6 +1227,8 @@ export function createTNode(
injectorIndex: tParent ? tParent.injectorIndex : -1, injectorIndex: tParent ? tParent.injectorIndex : -1,
directiveStart: -1, directiveStart: -1,
directiveEnd: -1, directiveEnd: -1,
propertyMetadataStartIndex: -1,
propertyMetadataEndIndex: -1,
flags: 0, flags: 0,
providerIndexes: 0, providerIndexes: 0,
tagName: tagName, tagName: tagName,
@ -2756,7 +2781,9 @@ export function markDirty<T>(component: T) {
*/ */
export function bind<T>(value: T): T|NO_CHANGE { export function bind<T>(value: T): T|NO_CHANGE {
const lView = getLView(); const lView = getLView();
return bindingUpdated(lView, lView[BINDING_INDEX]++, value) ? value : NO_CHANGE; const bindingIndex = lView[BINDING_INDEX]++;
storeBindingMetadata(lView);
return bindingUpdated(lView, bindingIndex, value) ? value : NO_CHANGE;
} }
/** /**
@ -2789,13 +2816,23 @@ export function interpolationV(values: any[]): string|NO_CHANGE {
ngDevMode && assertEqual(values.length % 2, 1, 'should have an odd number of values'); ngDevMode && assertEqual(values.length % 2, 1, 'should have an odd number of values');
let different = false; let different = false;
const lView = getLView(); const lView = getLView();
const tData = lView[TVIEW].data;
let bindingIndex = lView[BINDING_INDEX]; let bindingIndex = lView[BINDING_INDEX];
if (tData[bindingIndex] == null) {
// 2 is the index of the first static interstitial value (ie. not prefix)
for (let i = 2; i < values.length; i += 2) {
tData[bindingIndex++] = values[i];
}
bindingIndex = lView[BINDING_INDEX];
}
for (let i = 1; i < values.length; i += 2) { for (let i = 1; i < values.length; i += 2) {
// Check if bindings (odd indexes) have changed // Check if bindings (odd indexes) have changed
bindingUpdated(lView, bindingIndex++, values[i]) && (different = true); bindingUpdated(lView, bindingIndex++, values[i]) && (different = true);
} }
lView[BINDING_INDEX] = bindingIndex; lView[BINDING_INDEX] = bindingIndex;
storeBindingMetadata(lView, values[0], values[values.length - 1]);
if (!different) { if (!different) {
return NO_CHANGE; return NO_CHANGE;
@ -2819,8 +2856,8 @@ export function interpolationV(values: any[]): string|NO_CHANGE {
*/ */
export function interpolation1(prefix: string, v0: any, suffix: string): string|NO_CHANGE { export function interpolation1(prefix: string, v0: any, suffix: string): string|NO_CHANGE {
const lView = getLView(); const lView = getLView();
const different = bindingUpdated(lView, lView[BINDING_INDEX], v0); const different = bindingUpdated(lView, lView[BINDING_INDEX]++, v0);
lView[BINDING_INDEX] += 1; storeBindingMetadata(lView, prefix, suffix);
return different ? prefix + renderStringify(v0) + suffix : NO_CHANGE; return different ? prefix + renderStringify(v0) + suffix : NO_CHANGE;
} }
@ -2828,9 +2865,16 @@ export function interpolation1(prefix: string, v0: any, suffix: string): string|
export function interpolation2( export function interpolation2(
prefix: string, v0: any, i0: string, v1: any, suffix: string): string|NO_CHANGE { prefix: string, v0: any, i0: string, v1: any, suffix: string): string|NO_CHANGE {
const lView = getLView(); const lView = getLView();
const different = bindingUpdated2(lView, lView[BINDING_INDEX], v0, v1); const bindingIndex = lView[BINDING_INDEX];
const different = bindingUpdated2(lView, bindingIndex, v0, v1);
lView[BINDING_INDEX] += 2; lView[BINDING_INDEX] += 2;
// Only set static strings the first time (data will be null subsequent runs).
const data = storeBindingMetadata(lView, prefix, suffix);
if (data) {
lView[TVIEW].data[bindingIndex] = i0;
}
return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + suffix : NO_CHANGE; return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + suffix : NO_CHANGE;
} }
@ -2839,9 +2883,18 @@ export function interpolation3(
prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): string| prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): string|
NO_CHANGE { NO_CHANGE {
const lView = getLView(); const lView = getLView();
const different = bindingUpdated3(lView, lView[BINDING_INDEX], v0, v1, v2); const bindingIndex = lView[BINDING_INDEX];
const different = bindingUpdated3(lView, bindingIndex, v0, v1, v2);
lView[BINDING_INDEX] += 3; lView[BINDING_INDEX] += 3;
// Only set static strings the first time (data will be null subsequent runs).
const data = storeBindingMetadata(lView, prefix, suffix);
if (data) {
const tData = lView[TVIEW].data;
tData[bindingIndex] = i0;
tData[bindingIndex + 1] = i1;
}
return different ? return different ?
prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + suffix : prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + suffix :
NO_CHANGE; NO_CHANGE;
@ -2852,9 +2905,19 @@ export function interpolation4(
prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any,
suffix: string): string|NO_CHANGE { suffix: string): string|NO_CHANGE {
const lView = getLView(); const lView = getLView();
const different = bindingUpdated4(lView, lView[BINDING_INDEX], v0, v1, v2, v3); const bindingIndex = lView[BINDING_INDEX];
const different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3);
lView[BINDING_INDEX] += 4; lView[BINDING_INDEX] += 4;
// Only set static strings the first time (data will be null subsequent runs).
const data = storeBindingMetadata(lView, prefix, suffix);
if (data) {
const tData = lView[TVIEW].data;
tData[bindingIndex] = i0;
tData[bindingIndex + 1] = i1;
tData[bindingIndex + 2] = i2;
}
return different ? return different ?
prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 +
renderStringify(v3) + suffix : renderStringify(v3) + suffix :
@ -2871,6 +2934,16 @@ export function interpolation5(
different = bindingUpdated(lView, bindingIndex + 4, v4) || different; different = bindingUpdated(lView, bindingIndex + 4, v4) || different;
lView[BINDING_INDEX] += 5; lView[BINDING_INDEX] += 5;
// Only set static strings the first time (data will be null subsequent runs).
const data = storeBindingMetadata(lView, prefix, suffix);
if (data) {
const tData = lView[TVIEW].data;
tData[bindingIndex] = i0;
tData[bindingIndex + 1] = i1;
tData[bindingIndex + 2] = i2;
tData[bindingIndex + 3] = i3;
}
return different ? return different ?
prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 +
renderStringify(v3) + i3 + renderStringify(v4) + suffix : renderStringify(v3) + i3 + renderStringify(v4) + suffix :
@ -2887,6 +2960,17 @@ export function interpolation6(
different = bindingUpdated2(lView, bindingIndex + 4, v4, v5) || different; different = bindingUpdated2(lView, bindingIndex + 4, v4, v5) || different;
lView[BINDING_INDEX] += 6; lView[BINDING_INDEX] += 6;
// Only set static strings the first time (data will be null subsequent runs).
const data = storeBindingMetadata(lView, prefix, suffix);
if (data) {
const tData = lView[TVIEW].data;
tData[bindingIndex] = i0;
tData[bindingIndex + 1] = i1;
tData[bindingIndex + 2] = i2;
tData[bindingIndex + 3] = i3;
tData[bindingIndex + 4] = i4;
}
return different ? return different ?
prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 +
renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + suffix : renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + suffix :
@ -2904,6 +2988,18 @@ export function interpolation7(
different = bindingUpdated3(lView, bindingIndex + 4, v4, v5, v6) || different; different = bindingUpdated3(lView, bindingIndex + 4, v4, v5, v6) || different;
lView[BINDING_INDEX] += 7; lView[BINDING_INDEX] += 7;
// Only set static strings the first time (data will be null subsequent runs).
const data = storeBindingMetadata(lView, prefix, suffix);
if (data) {
const tData = lView[TVIEW].data;
tData[bindingIndex] = i0;
tData[bindingIndex + 1] = i1;
tData[bindingIndex + 2] = i2;
tData[bindingIndex + 3] = i3;
tData[bindingIndex + 4] = i4;
tData[bindingIndex + 5] = i5;
}
return different ? return different ?
prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 +
renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 + renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 +
@ -2922,6 +3018,19 @@ export function interpolation8(
different = bindingUpdated4(lView, bindingIndex + 4, v4, v5, v6, v7) || different; different = bindingUpdated4(lView, bindingIndex + 4, v4, v5, v6, v7) || different;
lView[BINDING_INDEX] += 8; lView[BINDING_INDEX] += 8;
// Only set static strings the first time (data will be null subsequent runs).
const data = storeBindingMetadata(lView, prefix, suffix);
if (data) {
const tData = lView[TVIEW].data;
tData[bindingIndex] = i0;
tData[bindingIndex + 1] = i1;
tData[bindingIndex + 2] = i2;
tData[bindingIndex + 3] = i3;
tData[bindingIndex + 4] = i4;
tData[bindingIndex + 5] = i5;
tData[bindingIndex + 6] = i6;
}
return different ? return different ?
prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 +
renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 + renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 +
@ -2929,6 +3038,30 @@ export function interpolation8(
NO_CHANGE; NO_CHANGE;
} }
/**
* Creates binding metadata for a particular binding and stores it in
* TView.data. These are generated in order to support DebugElement.properties.
*
* Each binding / interpolation will have one (including attribute bindings)
* because at the time of binding, we don't know to which instruction the binding
* belongs. It is always stored in TView.data at the index of the last binding
* value in LView (e.g. for interpolation8, it would be stored at the index of
* the 8th value).
*
* @param lView The LView that contains the current binding index.
* @param prefix The static prefix string
* @param suffix The static suffix string
*
* @returns Newly created binding metadata string for this binding or null
*/
function storeBindingMetadata(lView: LView, prefix = '', suffix = ''): string|null {
const tData = lView[TVIEW].data;
const lastBindingIndex = lView[BINDING_INDEX] - 1;
const value = INTERPOLATION_DELIMITER + prefix + INTERPOLATION_DELIMITER + suffix;
return tData[lastBindingIndex] == null ? (tData[lastBindingIndex] = value) : null;
}
/** Store a value in the `data` at a given `index`. */ /** Store a value in the `data` at a given `index`. */
export function store<T>(index: number, value: T): void { export function store<T>(index: number, value: T): void {
const lView = getLView(); const lView = getLView();

View File

@ -169,6 +169,18 @@ export interface TNode {
*/ */
directiveEnd: number; directiveEnd: number;
/**
* Stores the first index where property binding metadata is stored for
* this node.
*/
propertyMetadataStartIndex: number;
/**
* Stores the exclusive final index where property binding metadata is
* stored for this node.
*/
propertyMetadataEndIndex: number;
/** /**
* Stores if Node isComponent, isProjected, hasContentQuery and hasClassInput * Stores if Node isComponent, isProjected, hasContentQuery and hasClassInput
*/ */
@ -476,7 +488,6 @@ export type PropertyAliases = {
*/ */
export type PropertyAliasValue = (number | string)[]; export type PropertyAliasValue = (number | string)[];
/** /**
* This array contains information about input properties that * This array contains information about input properties that
* need to be set once from attribute data. It's ordered by * need to be set once from attribute data. It's ordered by

View File

@ -559,6 +559,18 @@ export type HookData = (number | (() => void))[];
* Each host property's name is stored here at the same index as its value in the * Each host property's name is stored here at the same index as its value in the
* data array. * data array.
* *
* Each property binding name is stored here at the same index as its value in
* the data array. If the binding is an interpolation, the static string values
* are stored parallel to the dynamic values. Example:
*
* id="prefix {{ v0 }} a {{ v1 }} b {{ v2 }} suffix"
*
* LView | TView.data
*------------------------
* v0 value | 'a'
* v1 value | 'b'
* v2 value | id <EFBFBD> prefix <EFBFBD> suffix
*
* Injector bloom filters are also stored here. * Injector bloom filters are also stored here.
*/ */
export type TData = export type TData =

View File

@ -291,4 +291,28 @@ export function resolveDocument(element: RElement & {ownerDocument: Document}) {
export function resolveBody(element: RElement & {ownerDocument: Document}) { export function resolveBody(element: RElement & {ownerDocument: Document}) {
return {name: 'body', target: element.ownerDocument.body}; return {name: 'body', target: element.ownerDocument.body};
} }
/**
* The special delimiter we use to separate property names, prefixes, and suffixes
* in property binding metadata. See storeBindingMetadata().
*
* We intentionally use the Unicode "REPLACEMENT CHARACTER" (U+FFFD) as a delimiter
* because it is a very uncommon character that is unlikely to be part of a user's
* property names or interpolation strings. If it is in fact used in a property
* binding, DebugElement.properties will not return the correct value for that
* binding. However, there should be no runtime effect for real applications.
*
* This character is typically rendered as a question mark inside of a diamond.
* See https://en.wikipedia.org/wiki/Specials_(Unicode_block)
*
*/
export const INTERPOLATION_DELIMITER = `<EFBFBD>`;
/**
* Determines whether or not the given string is a property metadata string.
* See storeBindingMetadata().
*/
export function isPropMetadataString(str: string): boolean {
return str.indexOf(INTERPOLATION_DELIMITER) >= 0;
}

View File

@ -77,6 +77,9 @@
{ {
"name": "INJECTOR_BLOOM_PARENT_SIZE" "name": "INJECTOR_BLOOM_PARENT_SIZE"
}, },
{
"name": "INTERPOLATION_DELIMITER"
},
{ {
"name": "InjectFlags" "name": "InjectFlags"
}, },
@ -1100,6 +1103,9 @@
{ {
"name": "saveNameToExportMap" "name": "saveNameToExportMap"
}, },
{
"name": "savePropertyDebugData"
},
{ {
"name": "saveResolvedLocalsInData" "name": "saveResolvedLocalsInData"
}, },
@ -1190,6 +1196,9 @@
{ {
"name": "shouldSearchParent" "name": "shouldSearchParent"
}, },
{
"name": "storeBindingMetadata"
},
{ {
"name": "storeCleanupFn" "name": "storeCleanupFn"
}, },

View File

@ -52,13 +52,22 @@ export class WithRefsCmp {
export class InheritedCmp extends SimpleCmp { export class InheritedCmp extends SimpleCmp {
} }
@Directive({selector: '[dir]', host: {'[id]': 'id'}}) @Directive({selector: '[hostBindingDir]', host: {'[id]': 'id'}})
export class HostBindingDir { export class HostBindingDir {
id = 'one'; id = 'one';
} }
@Component({selector: 'host-binding-parent', template: '<div dir></div>'}) @Component({
export class HostBindingParent { selector: 'component-with-prop-bindings',
template: `
<div hostBindingDir [title]="title" [attr.aria-label]="label"></div>
<p title="( {{ label }} - {{ title }} )" [attr.aria-label]="label" id="[ {{ label }} ] [ {{ title }} ]">
</p>
`
})
export class ComponentWithPropBindings {
title = 'some title';
label = 'some label';
} }
@Component({ @Component({
@ -72,7 +81,8 @@ export class SimpleApp {
@NgModule({ @NgModule({
declarations: [ declarations: [
HelloWorld, SimpleCmp, WithRefsCmp, InheritedCmp, SimpleApp, HostBindingParent, HostBindingDir HelloWorld, SimpleCmp, WithRefsCmp, InheritedCmp, SimpleApp, ComponentWithPropBindings,
HostBindingDir
], ],
imports: [GreetingModule], imports: [GreetingModule],
providers: [ providers: [
@ -123,12 +133,21 @@ describe('TestBed', () => {
expect(greetingByCss.nativeElement).toHaveText('Hello TestBed!'); expect(greetingByCss.nativeElement).toHaveText('Hello TestBed!');
}); });
it('should give the ability to access host properties', () => { it('should give the ability to access property bindings on a node', () => {
const fixture = TestBed.createComponent(HostBindingParent); const fixture = TestBed.createComponent(ComponentWithPropBindings);
fixture.detectChanges(); fixture.detectChanges();
const divElement = fixture.debugElement.children[0]; const divElement = fixture.debugElement.query(By.css('div'));
expect(divElement.properties).toEqual({id: 'one'}); expect(divElement.properties).toEqual({id: 'one', title: 'some title'});
});
it('should give the ability to access interpolated properties on a node', () => {
const fixture = TestBed.createComponent(ComponentWithPropBindings);
fixture.detectChanges();
const paragraphEl = fixture.debugElement.query(By.css('p'));
expect(paragraphEl.properties)
.toEqual({title: '( some label - some title )', id: '[ some label ] [ some title ]'});
}); });
it('should give access to the node injector', () => { it('should give access to the node injector', () => {