From a383a5a165348fd79ae03c8325ea7589560a9d13 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Tue, 3 Sep 2019 17:21:57 +0200 Subject: [PATCH] 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 --- packages/core/src/debug/debug_node.ts | 98 ++++--------------- .../src/render3/instructions/host_property.ts | 27 ++--- .../src/render3/instructions/interpolation.ts | 97 +----------------- .../src/render3/instructions/lview_debug.ts | 3 +- .../core/src/render3/instructions/property.ts | 16 +-- .../instructions/property_interpolation.ts | 89 +++++++++++++---- .../core/src/render3/instructions/shared.ts | 91 ++++++++--------- packages/core/src/render3/interfaces/node.ts | 14 +-- packages/core/src/render3/util/misc_utils.ts | 8 -- .../bundling/todo/bundle.golden_symbols.json | 5 +- packages/core/test/debug/debug_node_spec.ts | 53 ++++++++++ 11 files changed, 212 insertions(+), 289 deletions(-) diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts index c204f8ef706ea7..e724b960b6f852 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -17,7 +17,7 @@ import {stylingMapToStringMap} from '../render3/styling_next/map_based_bindings' import {NodeStylingDebug} from '../render3/styling_next/styling_debug'; import {isStylingContext} from '../render3/styling_next/util'; 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 {getComponentViewByIndex, getNativeByTNodeOrNull} from '../render3/util/view_utils'; 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 properties = collectPropertyBindings(tNode, lView, tData); - const hostProperties = collectHostPropertyBindings(tNode, lView, tData); const className = collectClassNames(this); - const output = {...properties, ...hostProperties}; 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;} { @@ -645,81 +644,26 @@ function _queryNativeNodeDescendants( 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: any; - 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 = (value || '') + renderStringify(lView[bindingIndex]) + tData[bindingIndex]; - propMetadata = tData[++bindingIndex] as string; - } - 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; + let bindingIndexes = tNode.propertyBindings; + + if (bindingIndexes !== null) { + for (let i = 0; i < bindingIndexes.length; i++) { + const bindingIndex = bindingIndexes[i]; + const propMetadata = tData[bindingIndex] as string; + const metadataParts = propMetadata.split(INTERPOLATION_DELIMITER); + const propertyName = metadataParts[0]; + if (metadataParts.length > 1) { + let value = metadataParts[1]; + 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]; + } } - 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; } diff --git a/packages/core/src/render3/instructions/host_property.ts b/packages/core/src/render3/instructions/host_property.ts index d89a97e80d2330..e2022637afd549 100644 --- a/packages/core/src/render3/instructions/host_property.ts +++ b/packages/core/src/render3/instructions/host_property.ts @@ -5,12 +5,13 @@ * 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 {assertNotEqual} from '../../util/assert'; +import {bindingUpdated} from '../bindings'; import {SanitizerFn} from '../interfaces/sanitization'; +import {BINDING_INDEX, TVIEW} from '../interfaces/view'; import {getLView, getSelectedIndex} from '../state'; 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. @@ -28,12 +29,12 @@ import {TsickleIssue1009, elementPropertyInternal, loadComponentRenderer} from ' */ export function ɵɵhostProperty( propName: string, value: T, sanitizer?: SanitizerFn | null): TsickleIssue1009 { - const index = getSelectedIndex(); - ngDevMode && assertNotEqual(index, -1, 'selected index cannot be -1'); const lView = getLView(); - const bindReconciledValue = bind(lView, value); - if (bindReconciledValue !== NO_CHANGE) { - elementPropertyInternal(index, propName, bindReconciledValue, sanitizer, true); + const bindingIndex = lView[BINDING_INDEX]++; + if (bindingUpdated(lView, bindingIndex, value)) { + const nodeIndex = getSelectedIndex(); + elementPropertyInternal(nodeIndex, propName, value, sanitizer, true); + ngDevMode && storePropertyBindingMetadata(lView[TVIEW].data, nodeIndex, propName, bindingIndex); } return ɵɵhostProperty; } @@ -62,12 +63,12 @@ export function ɵɵhostProperty( */ export function ɵɵupdateSyntheticHostBinding( propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null): TsickleIssue1009 { - const index = getSelectedIndex(); const lView = getLView(); - // TODO(benlesh): remove bind call here. - const bound = bind(lView, value); - if (bound !== NO_CHANGE) { - elementPropertyInternal(index, propName, bound, sanitizer, true, loadComponentRenderer); + const bindingIndex = lView[BINDING_INDEX]++; + if (bindingUpdated(lView, bindingIndex, value)) { + const nodeIndex = getSelectedIndex(); + elementPropertyInternal(nodeIndex, propName, value, sanitizer, true, loadComponentRenderer); + ngDevMode && storePropertyBindingMetadata(lView[TVIEW].data, nodeIndex, propName, bindingIndex); } return ɵɵupdateSyntheticHostBinding; } diff --git a/packages/core/src/render3/instructions/interpolation.ts b/packages/core/src/render3/instructions/interpolation.ts index 2dc3c12e53cf99..8e57f414d07367 100644 --- a/packages/core/src/render3/instructions/interpolation.ts +++ b/packages/core/src/render3/instructions/interpolation.ts @@ -8,10 +8,9 @@ import {assertEqual, assertLessThan} from '../../util/assert'; 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 {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 && assertEqual(values.length % 2, 1, 'should have an odd number of values'); let isBindingUpdated = false; - const tData = lView[TVIEW].data; 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) { // Check if bindings (odd indexes) have changed isBindingUpdated = bindingUpdated(lView, bindingIndex++, values[i]) || isBindingUpdated; } lView[BINDING_INDEX] = bindingIndex; - storeBindingMetadata(lView, values[0], values[values.length - 1]); if (!isBindingUpdated) { 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| NO_CHANGE { const different = bindingUpdated(lView, lView[BINDING_INDEX]++, v0); - ngDevMode && storeBindingMetadata(lView, prefix, suffix); return different ? prefix + renderStringify(v0) + suffix : NO_CHANGE; } @@ -85,14 +73,6 @@ export function interpolation2( const different = bindingUpdated2(lView, bindingIndex, v0, v1); 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; } @@ -106,16 +86,6 @@ export function interpolation3( const different = bindingUpdated3(lView, bindingIndex, v0, v1, v2); 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 ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + suffix : NO_CHANGE; @@ -131,17 +101,6 @@ export function interpolation4( const different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); 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 ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + renderStringify(v3) + suffix : @@ -159,18 +118,6 @@ export function interpolation5( different = bindingUpdated(lView, bindingIndex + 4, v4) || different; 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 ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + renderStringify(v3) + i3 + renderStringify(v4) + suffix : @@ -188,19 +135,6 @@ export function interpolation6( different = bindingUpdated2(lView, bindingIndex + 4, v4, v5) || different; 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 ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + 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; 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 ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + 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; 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 ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 + diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index 47feb1c3633cd8..9235f7e61e41c5 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -118,8 +118,7 @@ export const TNodeConstructor = class TNode implements ITNode { public injectorIndex: number, // public directiveStart: number, // public directiveEnd: number, // - public propertyMetadataStartIndex: number, // - public propertyMetadataEndIndex: number, // + public propertyBindings: number[]|null, // public flags: TNodeFlags, // public providerIndexes: TNodeProviderIndexes, // public tagName: string|null, // diff --git a/packages/core/src/render3/instructions/property.ts b/packages/core/src/render3/instructions/property.ts index a86864a4f79b08..0ae3e0537e837b 100644 --- a/packages/core/src/render3/instructions/property.ts +++ b/packages/core/src/render3/instructions/property.ts @@ -7,11 +7,11 @@ */ import {bindingUpdated} from '../bindings'; 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 {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( propName: string, value: T, sanitizer?: SanitizerFn | null): TsickleIssue1009 { const lView = getLView(); - const bindReconciledValue = bind(lView, value); - if (bindReconciledValue !== NO_CHANGE) { - elementPropertyInternal(getSelectedIndex(), propName, bindReconciledValue, sanitizer); + const bindingIndex = lView[BINDING_INDEX]++; + if (bindingUpdated(lView, bindingIndex, value)) { + const nodeIndex = getSelectedIndex(); + elementPropertyInternal(nodeIndex, propName, value, sanitizer); + ngDevMode && storePropertyBindingMetadata(lView[TVIEW].data, nodeIndex, propName, bindingIndex); } return ɵɵproperty; } @@ -49,7 +51,5 @@ export function ɵɵproperty( * @param value Value to diff */ export function bind(lView: LView, value: T): T|NO_CHANGE { - const bindingIndex = lView[BINDING_INDEX]++; - ngDevMode && storeBindingMetadata(lView); - return bindingUpdated(lView, bindingIndex, value) ? value : NO_CHANGE; + return bindingUpdated(lView, lView[BINDING_INDEX]++, value) ? value : NO_CHANGE; } diff --git a/packages/core/src/render3/instructions/property_interpolation.ts b/packages/core/src/render3/instructions/property_interpolation.ts index 5787487f432a90..60723361a80c4f 100644 --- a/packages/core/src/render3/instructions/property_interpolation.ts +++ b/packages/core/src/render3/instructions/property_interpolation.ts @@ -6,11 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ import {SanitizerFn} from '../interfaces/sanitization'; +import {BINDING_INDEX, TVIEW} from '../interfaces/view'; import {getLView, getSelectedIndex} from '../state'; import {NO_CHANGE} from '../tokens'; 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( propName: string, prefix: string, v0: any, suffix: string, sanitizer?: SanitizerFn): TsickleIssue1009 { - const interpolatedValue = interpolation1(getLView(), prefix, v0, suffix); + const lView = getLView(); + const interpolatedValue = interpolation1(lView, prefix, v0, suffix); if (interpolatedValue !== NO_CHANGE) { elementPropertyInternal(getSelectedIndex(), propName, interpolatedValue, sanitizer); + ngDevMode && storePropertyBindingMetadata( + lView[TVIEW].data, getSelectedIndex(), propName, lView[BINDING_INDEX] - 1, + prefix, suffix); } return ɵɵpropertyInterpolate1; } @@ -121,9 +126,14 @@ export function ɵɵpropertyInterpolate1( export function ɵɵpropertyInterpolate2( propName: string, prefix: string, v0: any, i0: string, v1: any, suffix: string, 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) { - 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; } @@ -164,9 +174,14 @@ export function ɵɵpropertyInterpolate2( export function ɵɵpropertyInterpolate3( propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, 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) { - 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; } @@ -209,9 +224,14 @@ export function ɵɵpropertyInterpolate3( export function ɵɵpropertyInterpolate4( propName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, 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) { - 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; } @@ -256,10 +276,15 @@ export function ɵɵpropertyInterpolate4( export function ɵɵpropertyInterpolate5( 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 { + const lView = getLView(); 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) { - 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; } @@ -307,10 +332,15 @@ export function ɵɵpropertyInterpolate6( 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, sanitizer?: SanitizerFn): TsickleIssue1009 { + const lView = getLView(); 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) { - 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; } @@ -360,10 +390,15 @@ export function ɵɵpropertyInterpolate7( 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, sanitizer?: SanitizerFn): TsickleIssue1009 { - const interpolatedValue = interpolation7( - getLView(), prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix); + const lView = getLView(); + const interpolatedValue = + interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix); 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; } @@ -415,10 +450,15 @@ export function ɵɵpropertyInterpolate8( 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, suffix: string, sanitizer?: SanitizerFn): TsickleIssue1009 { + const lView = getLView(); 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) { - 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; } @@ -455,9 +495,20 @@ export function ɵɵpropertyInterpolate8( */ export function ɵɵpropertyInterpolateV( propName: string, values: any[], sanitizer?: SanitizerFn): TsickleIssue1009 { - const interpolatedValue = interpolationV(getLView(), values); + const lView = getLView(); + const interpolatedValue = interpolationV(lView, values); 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; } diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index a320529f775ddb..c1f3c93071c278 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -759,8 +759,7 @@ export function createTNode( injectorIndex, // injectorIndex: number -1, // directiveStart: number -1, // directiveEnd: number - -1, // propertyMetadataStartIndex: number - -1, // propertyMetadataEndIndex: number + null, // propertyBindings: number[]|null 0, // flags: TNodeFlags 0, // providerIndexes: TNodeProviderIndexes tagName, // tagName: string|null @@ -784,8 +783,7 @@ export function createTNode( injectorIndex: injectorIndex, directiveStart: -1, directiveEnd: -1, - propertyMetadataStartIndex: -1, - propertyMetadataEndIndex: -1, + propertyBindings: null, flags: 0, providerIndexes: 0, tagName: tagName, @@ -860,11 +858,12 @@ export function elementPropertyInternal( loadRendererFn?: ((tNode: TNode, lView: LView) => Renderer3) | null): void { ngDevMode && assertNotSame(value, NO_CHANGE as any, 'Incoming value should never be NO_CHANGE.'); const lView = getLView(); + const tView = lView[TVIEW]; const element = getNativeByIndex(index, lView) as RElement | RComment; const tNode = getTNode(index, lView); let inputData: PropertyAliases|null|undefined; let dataValue: PropertyAliasValue|undefined; - if (!nativeOnly && (inputData = initializeTNodeInputs(lView[TVIEW], tNode)) && + if (!nativeOnly && (inputData = initializeTNodeInputs(tView, tNode)) && (dataValue = inputData[propName])) { setInputsForProperty(lView, dataValue, value); if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET); @@ -893,8 +892,6 @@ export function elementPropertyInternal( ngDevMode.rendererSetProperty++; } - ngDevMode && savePropertyDebugData(tNode, lView, propName, lView[TVIEW].data, nativeOnly); - const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER]; // It is assumed that the sanitizer is only added when the compiler determines that the // property @@ -982,34 +979,6 @@ function matchingSchemas(hostView: LView, tagName: string | null): boolean { 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. * @param propName Name of the invalid property. @@ -1758,27 +1727,45 @@ function executeViewQueryFn( /////////////////////////////// /** - * Creates binding metadata for a particular binding and stores it in - * TView.data. These are generated in order to support DebugElement.properties. + * Stores meta-data for a property binding to be used by TestBed's `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). + * In order to support TestBed's `DebugElement.properties` we need to save, for each binding: + * - a bound property name; + * - a static parts of interpolated strings; * - * @param lView The LView that contains the current binding index. - * @param prefix The static prefix string - * @param suffix The static suffix string + * A given property metadata is saved at the binding's index in the `TView.data` (in other words, a + * property binding metadata will be stored in `TView.data` at the same index as a bound value in + * `LView`). Metadata are represented as `INTERPOLATION_DELIMITER`-delimited string with the + * following format: + * - `propertyName` for bound properties; + * - `propertyName�prefix�interpolation_static_part1�..interpolation_static_partN�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 { - 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; +export function storePropertyBindingMetadata( + tData: TData, nodeIndex: number, propertyName: string, bindingIndex: number, + ...interpolationParts: string[]) { + // 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 + // 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; diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 6296fd7b66f8f0..3d311c14205941 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -260,19 +260,13 @@ export interface TNode { directiveEnd: number; /** - * Stores the first index where property binding metadata is stored for - * this node. + * Stores indexes of property bindings. This field is only set in the ngDevMode and holds indexes + * 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 - * stored for this node. - */ - propertyMetadataEndIndex: number; - - /** - * Stores if Node isComponent, isProjected, hasContentQuery, hasClassInput and hasStyleInput + * Stores if Node isComponent, isProjected, hasContentQuery, hasClassInput and hasStyleInput etc. */ flags: TNodeFlags; diff --git a/packages/core/src/render3/util/misc_utils.ts b/packages/core/src/render3/util/misc_utils.ts index 9ff7eb08c31ff1..5d0b9ea648cecd 100644 --- a/packages/core/src/render3/util/misc_utils.ts +++ b/packages/core/src/render3/util/misc_utils.ts @@ -82,14 +82,6 @@ export function ɵɵresolveBody(element: RElement & {ownerDocument: Document}) { */ export const INTERPOLATION_DELIMITER = `�`; -/** - * 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). */ diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 82bc7b031903e6..706e77adb84afc 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -488,9 +488,6 @@ { "name": "baseResolveDirective" }, - { - "name": "bind" - }, { "name": "bindingUpdated" }, @@ -1457,4 +1454,4 @@ { "name": "ɵɵtextInterpolate1" } -] +] \ No newline at end of file diff --git a/packages/core/test/debug/debug_node_spec.ts b/packages/core/test/debug/debug_node_spec.ts index ea5eba385e45cd..0b33aa17480dd7 100644 --- a/packages/core/test/debug/debug_node_spec.ts +++ b/packages/core/test/debug/debug_node_spec.ts @@ -33,6 +33,11 @@ class MessageDir { set message(newMessage: string) { this.logger.add(newMessage); } } +@Directive({selector: '[with-title]', inputs: ['title']}) +class WithTitleDir { + title = ''; +} + @Component({ selector: 'child-comp', template: `
@@ -211,6 +216,23 @@ class TestCmptWithPropBindings { title = 'hello'; } +@Component({ + template: ` + + + + + + + + + + +` +}) +class TestCmptWithPropInterpolation { +} + { describe('debug element', () => { let fixture: ComponentFixture; @@ -235,6 +257,8 @@ class TestCmptWithPropBindings { TestCmptWithViewContainerRef, SimpleContentComp, TestCmptWithPropBindings, + TestCmptWithPropInterpolation, + WithTitleDir, ], providers: [Logger], schemas: [NO_ERRORS_SCHEMA], @@ -720,6 +744,35 @@ class TestCmptWithPropBindings { 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, ``); + 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', () => { @Component({template: ''}) class TestComponent implements OnInit {