refactor(ivy): simplify property binding metadata storage (#32457)

Since property binding metadata storage is guarded with the ngDevMode now
and several instructions were merged together, we can simplify the way we
store and read property binding metadata.

PR Close #32457
This commit is contained in:
Pawel Kozlowski 2019-09-03 17:21:57 +02:00 committed by Miško Hevery
parent cfa09b84dd
commit a383a5a165
11 changed files with 211 additions and 288 deletions

View File

@ -17,7 +17,7 @@ import {stylingMapToStringMap} from '../render3/styling_next/map_based_bindings'
import {NodeStylingDebug} from '../render3/styling_next/styling_debug'; import {NodeStylingDebug} from '../render3/styling_next/styling_debug';
import {isStylingContext} from '../render3/styling_next/util'; import {isStylingContext} from '../render3/styling_next/util';
import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, isBrowserEvents, loadLContext} from '../render3/util/discovery_utils'; import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, isBrowserEvents, loadLContext} from '../render3/util/discovery_utils';
import {INTERPOLATION_DELIMITER, isPropMetadataString, renderStringify} from '../render3/util/misc_utils'; import {INTERPOLATION_DELIMITER, renderStringify} from '../render3/util/misc_utils';
import {findComponentView} from '../render3/util/view_traversal_utils'; import {findComponentView} from '../render3/util/view_traversal_utils';
import {getComponentViewByIndex, getNativeByTNodeOrNull} from '../render3/util/view_utils'; import {getComponentViewByIndex, getNativeByTNodeOrNull} from '../render3/util/view_utils';
import {assertDomNode} from '../util/assert'; import {assertDomNode} from '../util/assert';
@ -282,15 +282,14 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
const tNode = tData[context.nodeIndex] as TNode; const tNode = tData[context.nodeIndex] as TNode;
const properties = collectPropertyBindings(tNode, lView, tData); const properties = collectPropertyBindings(tNode, lView, tData);
const hostProperties = collectHostPropertyBindings(tNode, lView, tData);
const className = collectClassNames(this); const className = collectClassNames(this);
const output = {...properties, ...hostProperties};
if (className) { if (className) {
output['className'] = output['className'] ? output['className'] + ` ${className}` : className; properties['className'] =
properties['className'] ? properties['className'] + ` ${className}` : className;
} }
return output; return properties;
} }
get attributes(): {[key: string]: string | null;} { get attributes(): {[key: string]: string | null;} {
@ -645,81 +644,26 @@ function _queryNativeNodeDescendants(
function collectPropertyBindings( function collectPropertyBindings(
tNode: TNode, lView: LView, tData: TData): {[key: string]: string} { tNode: TNode, lView: LView, tData: TData): {[key: string]: string} {
const properties: {[key: string]: string} = {}; const properties: {[key: string]: string} = {};
let bindingIndex = getFirstBindingIndex(tNode.propertyMetadataStartIndex, tData); let bindingIndexes = tNode.propertyBindings;
while (bindingIndex < tNode.propertyMetadataEndIndex) { if (bindingIndexes !== null) {
let value: any; for (let i = 0; i < bindingIndexes.length; i++) {
let propMetadata = tData[bindingIndex] as string; const bindingIndex = bindingIndexes[i];
while (!isPropMetadataString(propMetadata)) { const propMetadata = tData[bindingIndex] as string;
// This is the first value for an interpolation. We need to build up const metadataParts = propMetadata.split(INTERPOLATION_DELIMITER);
// the full interpolation by combining runtime values in LView with const propertyName = metadataParts[0];
// the static interstitial values stored in TData. if (metadataParts.length > 1) {
value = (value || '') + renderStringify(lView[bindingIndex]) + tData[bindingIndex]; let value = metadataParts[1];
propMetadata = tData[++bindingIndex] as string; for (let j = 1; j < metadataParts.length - 1; j++) {
value += renderStringify(lView[bindingIndex + j - 1]) + metadataParts[j + 1];
}
properties[propertyName] = value;
} else {
properties[propertyName] = lView[bindingIndex];
}
} }
value = value === undefined ? lView[bindingIndex] : 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), if they're defined.
// Avoid wrapping for normal bindings so that the value doesn't get cast to a string.
properties[propertyName] = (metadataParts[1] && metadataParts[2]) ?
metadataParts[1] + value + metadataParts[2] :
value;
}
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; return properties;
} }

View File

@ -5,12 +5,13 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {assertNotEqual} from '../../util/assert'; import {bindingUpdated} from '../bindings';
import {SanitizerFn} from '../interfaces/sanitization'; import {SanitizerFn} from '../interfaces/sanitization';
import {BINDING_INDEX, TVIEW} from '../interfaces/view';
import {getLView, getSelectedIndex} from '../state'; import {getLView, getSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {bind} from './property';
import {TsickleIssue1009, elementPropertyInternal, loadComponentRenderer} from './shared'; import {TsickleIssue1009, elementPropertyInternal, loadComponentRenderer, storePropertyBindingMetadata} from './shared';
/** /**
* Update a property on a host element. Only applies to native node properties, not inputs. * Update a property on a host element. Only applies to native node properties, not inputs.
@ -28,12 +29,12 @@ import {TsickleIssue1009, elementPropertyInternal, loadComponentRenderer} from '
*/ */
export function ɵɵhostProperty<T>( export function ɵɵhostProperty<T>(
propName: string, value: T, sanitizer?: SanitizerFn | null): TsickleIssue1009 { propName: string, value: T, sanitizer?: SanitizerFn | null): TsickleIssue1009 {
const index = getSelectedIndex();
ngDevMode && assertNotEqual(index, -1, 'selected index cannot be -1');
const lView = getLView(); const lView = getLView();
const bindReconciledValue = bind(lView, value); const bindingIndex = lView[BINDING_INDEX]++;
if (bindReconciledValue !== NO_CHANGE) { if (bindingUpdated(lView, bindingIndex, value)) {
elementPropertyInternal(index, propName, bindReconciledValue, sanitizer, true); const nodeIndex = getSelectedIndex();
elementPropertyInternal(nodeIndex, propName, value, sanitizer, true);
ngDevMode && storePropertyBindingMetadata(lView[TVIEW].data, nodeIndex, propName, bindingIndex);
} }
return ɵɵhostProperty; return ɵɵhostProperty;
} }
@ -62,12 +63,12 @@ export function ɵɵhostProperty<T>(
*/ */
export function ɵɵupdateSyntheticHostBinding<T>( export function ɵɵupdateSyntheticHostBinding<T>(
propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null): TsickleIssue1009 { propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null): TsickleIssue1009 {
const index = getSelectedIndex();
const lView = getLView(); const lView = getLView();
// TODO(benlesh): remove bind call here. const bindingIndex = lView[BINDING_INDEX]++;
const bound = bind(lView, value); if (bindingUpdated(lView, bindingIndex, value)) {
if (bound !== NO_CHANGE) { const nodeIndex = getSelectedIndex();
elementPropertyInternal(index, propName, bound, sanitizer, true, loadComponentRenderer); elementPropertyInternal(nodeIndex, propName, value, sanitizer, true, loadComponentRenderer);
ngDevMode && storePropertyBindingMetadata(lView[TVIEW].data, nodeIndex, propName, bindingIndex);
} }
return ɵɵupdateSyntheticHostBinding; return ɵɵupdateSyntheticHostBinding;
} }

View File

@ -8,10 +8,9 @@
import {assertEqual, assertLessThan} from '../../util/assert'; import {assertEqual, assertLessThan} from '../../util/assert';
import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from '../bindings'; import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from '../bindings';
import {BINDING_INDEX, LView, TVIEW} from '../interfaces/view'; import {BINDING_INDEX, LView} from '../interfaces/view';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils'; import {renderStringify} from '../util/misc_utils';
import {storeBindingMetadata} from './shared';
@ -31,23 +30,13 @@ export function interpolationV(lView: LView, values: any[]): string|NO_CHANGE {
ngDevMode && assertLessThan(2, values.length, 'should have at least 3 values'); ngDevMode && assertLessThan(2, values.length, 'should have at least 3 values');
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 isBindingUpdated = false; let isBindingUpdated = false;
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
isBindingUpdated = bindingUpdated(lView, bindingIndex++, values[i]) || isBindingUpdated; isBindingUpdated = bindingUpdated(lView, bindingIndex++, values[i]) || isBindingUpdated;
} }
lView[BINDING_INDEX] = bindingIndex; lView[BINDING_INDEX] = bindingIndex;
storeBindingMetadata(lView, values[0], values[values.length - 1]);
if (!isBindingUpdated) { if (!isBindingUpdated) {
return NO_CHANGE; return NO_CHANGE;
@ -72,7 +61,6 @@ export function interpolationV(lView: LView, values: any[]): string|NO_CHANGE {
export function interpolation1(lView: LView, prefix: string, v0: any, suffix: string): string| export function interpolation1(lView: LView, prefix: string, v0: any, suffix: string): string|
NO_CHANGE { NO_CHANGE {
const different = bindingUpdated(lView, lView[BINDING_INDEX]++, v0); const different = bindingUpdated(lView, lView[BINDING_INDEX]++, v0);
ngDevMode && storeBindingMetadata(lView, prefix, suffix);
return different ? prefix + renderStringify(v0) + suffix : NO_CHANGE; return different ? prefix + renderStringify(v0) + suffix : NO_CHANGE;
} }
@ -85,14 +73,6 @@ export function interpolation2(
const different = bindingUpdated2(lView, bindingIndex, v0, v1); const different = bindingUpdated2(lView, bindingIndex, v0, v1);
lView[BINDING_INDEX] += 2; lView[BINDING_INDEX] += 2;
if (ngDevMode) {
// 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;
} }
@ -106,16 +86,6 @@ export function interpolation3(
const different = bindingUpdated3(lView, bindingIndex, v0, v1, v2); const different = bindingUpdated3(lView, bindingIndex, v0, v1, v2);
lView[BINDING_INDEX] += 3; lView[BINDING_INDEX] += 3;
if (ngDevMode) {
// 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;
@ -131,17 +101,6 @@ export function interpolation4(
const different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); const different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3);
lView[BINDING_INDEX] += 4; lView[BINDING_INDEX] += 4;
if (ngDevMode) {
// 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 :
@ -159,18 +118,6 @@ 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;
if (ngDevMode) {
// 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 :
@ -188,19 +135,6 @@ 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;
if (ngDevMode) {
// 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 :
@ -219,20 +153,6 @@ 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;
if (ngDevMode) {
// 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 +
@ -252,21 +172,6 @@ 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;
if (ngDevMode) {
// 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 +

View File

@ -118,8 +118,7 @@ export const TNodeConstructor = class TNode implements ITNode {
public injectorIndex: number, // public injectorIndex: number, //
public directiveStart: number, // public directiveStart: number, //
public directiveEnd: number, // public directiveEnd: number, //
public propertyMetadataStartIndex: number, // public propertyBindings: number[]|null, //
public propertyMetadataEndIndex: number, //
public flags: TNodeFlags, // public flags: TNodeFlags, //
public providerIndexes: TNodeProviderIndexes, // public providerIndexes: TNodeProviderIndexes, //
public tagName: string|null, // public tagName: string|null, //

View File

@ -7,11 +7,11 @@
*/ */
import {bindingUpdated} from '../bindings'; import {bindingUpdated} from '../bindings';
import {SanitizerFn} from '../interfaces/sanitization'; import {SanitizerFn} from '../interfaces/sanitization';
import {BINDING_INDEX, LView} from '../interfaces/view'; import {BINDING_INDEX, LView, TVIEW} from '../interfaces/view';
import {getLView, getSelectedIndex} from '../state'; import {getLView, getSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {TsickleIssue1009, elementPropertyInternal, storeBindingMetadata} from './shared'; import {TsickleIssue1009, elementPropertyInternal, storePropertyBindingMetadata} from './shared';
/** /**
@ -35,9 +35,11 @@ import {TsickleIssue1009, elementPropertyInternal, storeBindingMetadata} from '.
export function ɵɵproperty<T>( export function ɵɵproperty<T>(
propName: string, value: T, sanitizer?: SanitizerFn | null): TsickleIssue1009 { propName: string, value: T, sanitizer?: SanitizerFn | null): TsickleIssue1009 {
const lView = getLView(); const lView = getLView();
const bindReconciledValue = bind(lView, value); const bindingIndex = lView[BINDING_INDEX]++;
if (bindReconciledValue !== NO_CHANGE) { if (bindingUpdated(lView, bindingIndex, value)) {
elementPropertyInternal(getSelectedIndex(), propName, bindReconciledValue, sanitizer); const nodeIndex = getSelectedIndex();
elementPropertyInternal(nodeIndex, propName, value, sanitizer);
ngDevMode && storePropertyBindingMetadata(lView[TVIEW].data, nodeIndex, propName, bindingIndex);
} }
return ɵɵproperty; return ɵɵproperty;
} }
@ -49,7 +51,5 @@ export function ɵɵproperty<T>(
* @param value Value to diff * @param value Value to diff
*/ */
export function bind<T>(lView: LView, value: T): T|NO_CHANGE { export function bind<T>(lView: LView, value: T): T|NO_CHANGE {
const bindingIndex = lView[BINDING_INDEX]++; return bindingUpdated(lView, lView[BINDING_INDEX]++, value) ? value : NO_CHANGE;
ngDevMode && storeBindingMetadata(lView);
return bindingUpdated(lView, bindingIndex, value) ? value : NO_CHANGE;
} }

View File

@ -6,11 +6,12 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {SanitizerFn} from '../interfaces/sanitization'; import {SanitizerFn} from '../interfaces/sanitization';
import {BINDING_INDEX, TVIEW} from '../interfaces/view';
import {getLView, getSelectedIndex} from '../state'; import {getLView, getSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation'; import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation';
import {TsickleIssue1009, elementPropertyInternal} from './shared'; import {TsickleIssue1009, elementPropertyInternal, storePropertyBindingMetadata} from './shared';
@ -81,9 +82,13 @@ export function ɵɵpropertyInterpolate(
export function ɵɵpropertyInterpolate1( export function ɵɵpropertyInterpolate1(
propName: string, prefix: string, v0: any, suffix: string, propName: string, prefix: string, v0: any, suffix: string,
sanitizer?: SanitizerFn): TsickleIssue1009 { sanitizer?: SanitizerFn): TsickleIssue1009 {
const interpolatedValue = interpolation1(getLView(), prefix, v0, suffix); const lView = getLView();
const interpolatedValue = interpolation1(lView, prefix, v0, suffix);
if (interpolatedValue !== NO_CHANGE) { if (interpolatedValue !== NO_CHANGE) {
elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer); elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, getSelectedIndex(), propName, lView[BINDING_INDEX] - 1,
prefix, suffix);
} }
return ɵɵpropertyInterpolate1; return ɵɵpropertyInterpolate1;
} }
@ -121,9 +126,14 @@ export function ɵɵpropertyInterpolate1(
export function ɵɵpropertyInterpolate2( export function ɵɵpropertyInterpolate2(
propName: string, prefix: string, v0: any, i0: string, v1: any, suffix: string, propName: string, prefix: string, v0: any, i0: string, v1: any, suffix: string,
sanitizer?: SanitizerFn): TsickleIssue1009 { sanitizer?: SanitizerFn): TsickleIssue1009 {
const interpolatedValue = interpolation2(getLView(), prefix, v0, i0, v1, suffix); const lView = getLView();
const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix);
if (interpolatedValue !== NO_CHANGE) { if (interpolatedValue !== NO_CHANGE) {
elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer); const nodeIndex = getSelectedIndex();
elementPropertyInternal(nodeIndex, propName, interpolatedValue, sanitizer);
ngDevMode &&
storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, propName, lView[BINDING_INDEX] - 2, prefix, i0, suffix);
} }
return ɵɵpropertyInterpolate2; return ɵɵpropertyInterpolate2;
} }
@ -164,9 +174,14 @@ export function ɵɵpropertyInterpolate2(
export function ɵɵpropertyInterpolate3( export function ɵɵpropertyInterpolate3(
propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any,
suffix: string, sanitizer?: SanitizerFn): TsickleIssue1009 { suffix: string, sanitizer?: SanitizerFn): TsickleIssue1009 {
const interpolatedValue = interpolation3(getLView(), prefix, v0, i0, v1, i1, v2, suffix); const lView = getLView();
const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix);
if (interpolatedValue !== NO_CHANGE) { if (interpolatedValue !== NO_CHANGE) {
elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer); const nodeIndex = getSelectedIndex();
elementPropertyInternal(nodeIndex, propName, interpolatedValue, sanitizer);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, propName, lView[BINDING_INDEX] - 3, prefix, i0,
i1, suffix);
} }
return ɵɵpropertyInterpolate3; return ɵɵpropertyInterpolate3;
} }
@ -209,9 +224,14 @@ export function ɵɵpropertyInterpolate3(
export function ɵɵpropertyInterpolate4( export function ɵɵpropertyInterpolate4(
propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string,
v3: any, suffix: string, sanitizer?: SanitizerFn): TsickleIssue1009 { v3: any, suffix: string, sanitizer?: SanitizerFn): TsickleIssue1009 {
const interpolatedValue = interpolation4(getLView(), prefix, v0, i0, v1, i1, v2, i2, v3, suffix); const lView = getLView();
const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix);
if (interpolatedValue !== NO_CHANGE) { if (interpolatedValue !== NO_CHANGE) {
elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer); const nodeIndex = getSelectedIndex();
elementPropertyInternal(nodeIndex, propName, interpolatedValue, sanitizer);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, propName, lView[BINDING_INDEX] - 4, prefix, i0,
i1, i2, suffix);
} }
return ɵɵpropertyInterpolate4; return ɵɵpropertyInterpolate4;
} }
@ -256,10 +276,15 @@ export function ɵɵpropertyInterpolate4(
export function ɵɵpropertyInterpolate5( export function ɵɵpropertyInterpolate5(
propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string,
v3: any, i3: string, v4: any, suffix: string, sanitizer?: SanitizerFn): TsickleIssue1009 { v3: any, i3: string, v4: any, suffix: string, sanitizer?: SanitizerFn): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue = const interpolatedValue =
interpolation5(getLView(), prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix); interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix);
if (interpolatedValue !== NO_CHANGE) { if (interpolatedValue !== NO_CHANGE) {
elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer); const nodeIndex = getSelectedIndex();
elementPropertyInternal(nodeIndex, propName, interpolatedValue, sanitizer);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, propName, lView[BINDING_INDEX] - 5, prefix, i0,
i1, i2, i3, suffix);
} }
return ɵɵpropertyInterpolate5; return ɵɵpropertyInterpolate5;
} }
@ -307,10 +332,15 @@ export function ɵɵpropertyInterpolate6(
propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string,
v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string, v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string,
sanitizer?: SanitizerFn): TsickleIssue1009 { sanitizer?: SanitizerFn): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue = const interpolatedValue =
interpolation6(getLView(), prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix); interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix);
if (interpolatedValue !== NO_CHANGE) { if (interpolatedValue !== NO_CHANGE) {
elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer); const nodeIndex = getSelectedIndex();
elementPropertyInternal(nodeIndex, propName, interpolatedValue, sanitizer);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, propName, lView[BINDING_INDEX] - 6, prefix, i0,
i1, i2, i3, i4, suffix);
} }
return ɵɵpropertyInterpolate6; return ɵɵpropertyInterpolate6;
} }
@ -360,10 +390,15 @@ export function ɵɵpropertyInterpolate7(
propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string,
v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string,
sanitizer?: SanitizerFn): TsickleIssue1009 { sanitizer?: SanitizerFn): TsickleIssue1009 {
const interpolatedValue = interpolation7( const lView = getLView();
getLView(), prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix); const interpolatedValue =
interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix);
if (interpolatedValue !== NO_CHANGE) { if (interpolatedValue !== NO_CHANGE) {
elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer); const nodeIndex = getSelectedIndex();
elementPropertyInternal(nodeIndex, propName, interpolatedValue, sanitizer);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, propName, lView[BINDING_INDEX] - 7, prefix, i0,
i1, i2, i3, i4, i5, suffix);
} }
return ɵɵpropertyInterpolate7; return ɵɵpropertyInterpolate7;
} }
@ -415,10 +450,15 @@ export function ɵɵpropertyInterpolate8(
propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string,
v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any,
suffix: string, sanitizer?: SanitizerFn): TsickleIssue1009 { suffix: string, sanitizer?: SanitizerFn): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue = interpolation8( const interpolatedValue = interpolation8(
getLView(), prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix); lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix);
if (interpolatedValue !== NO_CHANGE) { if (interpolatedValue !== NO_CHANGE) {
elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer); const nodeIndex = getSelectedIndex();
elementPropertyInternal(nodeIndex, propName, interpolatedValue, sanitizer);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, propName, lView[BINDING_INDEX] - 8, prefix, i0,
i1, i2, i3, i4, i5, i6, suffix);
} }
return ɵɵpropertyInterpolate8; return ɵɵpropertyInterpolate8;
} }
@ -455,9 +495,20 @@ export function ɵɵpropertyInterpolate8(
*/ */
export function ɵɵpropertyInterpolateV( export function ɵɵpropertyInterpolateV(
propName: string, values: any[], sanitizer?: SanitizerFn): TsickleIssue1009 { propName: string, values: any[], sanitizer?: SanitizerFn): TsickleIssue1009 {
const interpolatedValue = interpolationV(getLView(), values); const lView = getLView();
const interpolatedValue = interpolationV(lView, values);
if (interpolatedValue !== NO_CHANGE) { if (interpolatedValue !== NO_CHANGE) {
elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer); const nodeIndex = getSelectedIndex();
elementPropertyInternal(nodeIndex, propName, interpolatedValue, sanitizer);
if (ngDevMode) {
const interpolationInBetween = [values[0]]; // prefix
for (let i = 2; i < values.length; i += 2) {
interpolationInBetween.push(values[i]);
}
storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, propName,
lView[BINDING_INDEX] - interpolationInBetween.length + 1, ...interpolationInBetween);
}
} }
return ɵɵpropertyInterpolateV; return ɵɵpropertyInterpolateV;
} }

View File

@ -759,8 +759,7 @@ export function createTNode(
injectorIndex, // injectorIndex: number injectorIndex, // injectorIndex: number
-1, // directiveStart: number -1, // directiveStart: number
-1, // directiveEnd: number -1, // directiveEnd: number
-1, // propertyMetadataStartIndex: number null, // propertyBindings: number[]|null
-1, // propertyMetadataEndIndex: number
0, // flags: TNodeFlags 0, // flags: TNodeFlags
0, // providerIndexes: TNodeProviderIndexes 0, // providerIndexes: TNodeProviderIndexes
tagName, // tagName: string|null tagName, // tagName: string|null
@ -784,8 +783,7 @@ export function createTNode(
injectorIndex: injectorIndex, injectorIndex: injectorIndex,
directiveStart: -1, directiveStart: -1,
directiveEnd: -1, directiveEnd: -1,
propertyMetadataStartIndex: -1, propertyBindings: null,
propertyMetadataEndIndex: -1,
flags: 0, flags: 0,
providerIndexes: 0, providerIndexes: 0,
tagName: tagName, tagName: tagName,
@ -860,11 +858,12 @@ export function elementPropertyInternal<T>(
loadRendererFn?: ((tNode: TNode, lView: LView) => Renderer3) | null): void { loadRendererFn?: ((tNode: TNode, lView: LView) => Renderer3) | null): void {
ngDevMode && assertNotSame(value, NO_CHANGE as any, 'Incoming value should never be NO_CHANGE.'); ngDevMode && assertNotSame(value, NO_CHANGE as any, 'Incoming value should never be NO_CHANGE.');
const lView = getLView(); const lView = getLView();
const tView = lView[TVIEW];
const element = getNativeByIndex(index, lView) as RElement | RComment; const element = getNativeByIndex(index, lView) as RElement | RComment;
const tNode = getTNode(index, lView); const tNode = getTNode(index, lView);
let inputData: PropertyAliases|null|undefined; let inputData: PropertyAliases|null|undefined;
let dataValue: PropertyAliasValue|undefined; let dataValue: PropertyAliasValue|undefined;
if (!nativeOnly && (inputData = initializeTNodeInputs(lView[TVIEW], tNode)) && if (!nativeOnly && (inputData = initializeTNodeInputs(tView, tNode)) &&
(dataValue = inputData[propName])) { (dataValue = inputData[propName])) {
setInputsForProperty(lView, dataValue, value); setInputsForProperty(lView, dataValue, value);
if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET); if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET);
@ -893,8 +892,6 @@ export function elementPropertyInternal<T>(
ngDevMode.rendererSetProperty++; ngDevMode.rendererSetProperty++;
} }
ngDevMode && savePropertyDebugData(tNode, lView, propName, lView[TVIEW].data, nativeOnly);
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 // It is assumed that the sanitizer is only added when the compiler determines that the
// property // property
@ -982,34 +979,6 @@ function matchingSchemas(hostView: LView, tagName: string | null): boolean {
return false; return false;
} }
/**
* 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;
}
}
}
/** /**
* Creates an error that should be thrown when encountering an unknown property on an element. * Creates an error that should be thrown when encountering an unknown property on an element.
* @param propName Name of the invalid property. * @param propName Name of the invalid property.
@ -1758,27 +1727,45 @@ function executeViewQueryFn<T>(
/////////////////////////////// ///////////////////////////////
/** /**
* Creates binding metadata for a particular binding and stores it in * Stores meta-data for a property binding to be used by TestBed's `DebugElement.properties`.
* TView.data. These are generated in order to support DebugElement.properties.
* *
* Each binding / interpolation will have one (including attribute bindings) * In order to support TestBed's `DebugElement.properties` we need to save, for each binding:
* because at the time of binding, we don't know to which instruction the binding * - a bound property name;
* belongs. It is always stored in TView.data at the index of the last binding * - a static parts of interpolated strings;
* 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. * A given property metadata is saved at the binding's index in the `TView.data` (in other words, a
* @param prefix The static prefix string * property binding metadata will be stored in `TView.data` at the same index as a bound value in
* @param suffix The static suffix string * `LView`). Metadata are represented as `INTERPOLATION_DELIMITER`-delimited string with the
* following format:
* - `propertyName` for bound properties;
* - `propertyName<EFBFBD>prefix<EFBFBD>interpolation_static_part1<EFBFBD>..interpolation_static_partN<74>suffix` for
* interpolated properties.
* *
* @returns Newly created binding metadata string for this binding or null * @param tData `TData` where meta-data will be saved;
* @param nodeIndex index of a `TNode` that is a target of the binding;
* @param propertyName bound property name;
* @param bindingIndex binding index in `LView`
* @param interpolationParts static interpolation parts (for property interpolations)
*/ */
export function storeBindingMetadata(lView: LView, prefix = '', suffix = ''): string|null { export function storePropertyBindingMetadata(
const tData = lView[TVIEW].data; tData: TData, nodeIndex: number, propertyName: string, bindingIndex: number,
const lastBindingIndex = lView[BINDING_INDEX] - 1; ...interpolationParts: string[]) {
const value = INTERPOLATION_DELIMITER + prefix + INTERPOLATION_DELIMITER + suffix; // Binding meta-data are stored only the first time a given property instruction is processed.
// Since we don't have a concept of the "first update pass" we need to check for presence of the
return tData[lastBindingIndex] == null ? (tData[lastBindingIndex] = value) : null; // binding meta-data to decide if one should be stored (or if was stored already).
if (tData[bindingIndex] === null) {
const tNode = tData[nodeIndex + HEADER_OFFSET] as TNode;
if (tNode.inputs == null || !tNode.inputs[propertyName]) {
const propBindingIdxs = tNode.propertyBindings || (tNode.propertyBindings = []);
propBindingIdxs.push(bindingIndex);
let bindingMetadata = propertyName;
if (interpolationParts.length > 0) {
bindingMetadata +=
INTERPOLATION_DELIMITER + interpolationParts.join(INTERPOLATION_DELIMITER);
}
tData[bindingIndex] = bindingMetadata;
}
}
} }
export const CLEAN_PROMISE = _CLEAN_PROMISE; export const CLEAN_PROMISE = _CLEAN_PROMISE;

View File

@ -260,19 +260,13 @@ export interface TNode {
directiveEnd: number; directiveEnd: number;
/** /**
* Stores the first index where property binding metadata is stored for * Stores indexes of property bindings. This field is only set in the ngDevMode and holds indexes
* this node. * of property bindings so TestBed can get bound property metadata for a given node.
*/ */
propertyMetadataStartIndex: number; propertyBindings: number[]|null;
/** /**
* Stores the exclusive final index where property binding metadata is * Stores if Node isComponent, isProjected, hasContentQuery, hasClassInput and hasStyleInput etc.
* stored for this node.
*/
propertyMetadataEndIndex: number;
/**
* Stores if Node isComponent, isProjected, hasContentQuery, hasClassInput and hasStyleInput
*/ */
flags: TNodeFlags; flags: TNodeFlags;

View File

@ -82,14 +82,6 @@ export function ɵɵresolveBody(element: RElement & {ownerDocument: Document}) {
*/ */
export const INTERPOLATION_DELIMITER = `<EFBFBD>`; 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;
}
/** /**
* Unwrap a value which might be behind a closure (for forward declaration reasons). * Unwrap a value which might be behind a closure (for forward declaration reasons).
*/ */

View File

@ -488,9 +488,6 @@
{ {
"name": "baseResolveDirective" "name": "baseResolveDirective"
}, },
{
"name": "bind"
},
{ {
"name": "bindingUpdated" "name": "bindingUpdated"
}, },
@ -1457,4 +1454,4 @@
{ {
"name": "ɵɵtextInterpolate1" "name": "ɵɵtextInterpolate1"
} }
] ]

View File

@ -33,6 +33,11 @@ class MessageDir {
set message(newMessage: string) { this.logger.add(newMessage); } set message(newMessage: string) { this.logger.add(newMessage); }
} }
@Directive({selector: '[with-title]', inputs: ['title']})
class WithTitleDir {
title = '';
}
@Component({ @Component({
selector: 'child-comp', selector: 'child-comp',
template: `<div class="child" message="child"> template: `<div class="child" message="child">
@ -211,6 +216,23 @@ class TestCmptWithPropBindings {
title = 'hello'; title = 'hello';
} }
@Component({
template: `
<button title="{{0}}"></button>
<button title="a{{1}}b"></button>
<button title="a{{1}}b{{2}}c"></button>
<button title="a{{1}}b{{2}}c{{3}}d"></button>
<button title="a{{1}}b{{2}}c{{3}}d{{4}}e"></button>
<button title="a{{1}}b{{2}}c{{3}}d{{4}}e{{5}}f"></button>
<button title="a{{1}}b{{2}}c{{3}}d{{4}}e{{5}}f{{6}}g"></button>
<button title="a{{1}}b{{2}}c{{3}}d{{4}}e{{5}}f{{6}}g{{7}}h"></button>
<button title="a{{1}}b{{2}}c{{3}}d{{4}}e{{5}}f{{6}}g{{7}}h{{8}}i"></button>
<button title="a{{1}}b{{2}}c{{3}}d{{4}}e{{5}}f{{6}}g{{7}}h{{8}}i{{9}}j"></button>
`
})
class TestCmptWithPropInterpolation {
}
{ {
describe('debug element', () => { describe('debug element', () => {
let fixture: ComponentFixture<any>; let fixture: ComponentFixture<any>;
@ -235,6 +257,8 @@ class TestCmptWithPropBindings {
TestCmptWithViewContainerRef, TestCmptWithViewContainerRef,
SimpleContentComp, SimpleContentComp,
TestCmptWithPropBindings, TestCmptWithPropBindings,
TestCmptWithPropInterpolation,
WithTitleDir,
], ],
providers: [Logger], providers: [Logger],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
@ -720,6 +744,35 @@ class TestCmptWithPropBindings {
expect(button.properties).toEqual({disabled: true, tabIndex: 1337, title: 'hello'}); expect(button.properties).toEqual({disabled: true, tabIndex: 1337, title: 'hello'});
}); });
it('should include interpolated properties in the properties map', () => {
const fixture = TestBed.createComponent(TestCmptWithPropInterpolation);
fixture.detectChanges();
const buttons = fixture.debugElement.children;
expect(buttons.length).toBe(10);
expect(buttons[0].properties.title).toBe('0');
expect(buttons[1].properties.title).toBe('a1b');
expect(buttons[2].properties.title).toBe('a1b2c');
expect(buttons[3].properties.title).toBe('a1b2c3d');
expect(buttons[4].properties.title).toBe('a1b2c3d4e');
expect(buttons[5].properties.title).toBe('a1b2c3d4e5f');
expect(buttons[6].properties.title).toBe('a1b2c3d4e5f6g');
expect(buttons[7].properties.title).toBe('a1b2c3d4e5f6g7h');
expect(buttons[8].properties.title).toBe('a1b2c3d4e5f6g7h8i');
expect(buttons[9].properties.title).toBe('a1b2c3d4e5f6g7h8i9j');
});
it('should not include directive-shadowed properties in the properties map', () => {
TestBed.overrideTemplate(
TestCmptWithPropInterpolation, `<button with-title [title]="'goes to input'"></button>`);
const fixture = TestBed.createComponent(TestCmptWithPropInterpolation);
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('button'));
expect(button.properties.title).toBeUndefined();
});
it('should trigger events registered via Renderer2', () => { it('should trigger events registered via Renderer2', () => {
@Component({template: ''}) @Component({template: ''})
class TestComponent implements OnInit { class TestComponent implements OnInit {