feat(ivy): improve `ExpressionChangedAfterChecked` error message for attributes (#34505)

This commit improves `ExpressionChangedAfterChecked` error message for attributes by including attribute name and the content of the entire expression that contains interpolation(s). In order to achieve that, metadata is now stored in `TData` array when `attribute` and `attributeInterpolate` instructions are being called (similar to `property` and `propertyInterpolate` instructions).

PR Close #34505
This commit is contained in:
Andrew Kushnir 2019-12-19 17:02:51 -08:00 committed by atscott
parent 181d766941
commit c3f14bb7a5
4 changed files with 70 additions and 32 deletions

View File

@ -7,9 +7,10 @@
*/
import {bindingUpdated} from '../bindings';
import {SanitizerFn} from '../interfaces/sanitization';
import {TVIEW} from '../interfaces/view';
import {getLView, getSelectedIndex, nextBindingIndex} from '../state';
import {elementAttributeInternal} from './shared';
import {elementAttributeInternal, storePropertyBindingMetadata} from './shared';
@ -30,8 +31,12 @@ export function ɵɵattribute(
name: string, value: any, sanitizer?: SanitizerFn | null,
namespace?: string): typeof ɵɵattribute {
const lView = getLView();
if (bindingUpdated(lView, nextBindingIndex(), value)) {
elementAttributeInternal(getSelectedIndex(), name, value, lView, sanitizer, namespace);
const bindingIndex = nextBindingIndex();
if (bindingUpdated(lView, bindingIndex, value)) {
const nodeIndex = getSelectedIndex();
elementAttributeInternal(nodeIndex, name, value, lView, sanitizer, namespace);
ngDevMode &&
storePropertyBindingMetadata(lView[TVIEW].data, nodeIndex, 'attr.' + name, bindingIndex);
}
return ɵɵattribute;
}

View File

@ -6,11 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {SanitizerFn} from '../interfaces/sanitization';
import {getLView, getSelectedIndex} from '../state';
import {TVIEW} from '../interfaces/view';
import {getBindingIndex, getLView, getSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation';
import {elementAttributeInternal} from './shared';
import {elementAttributeInternal, storePropertyBindingMetadata} from './shared';
@ -44,8 +45,11 @@ export function ɵɵattributeInterpolate1(
const lView = getLView();
const interpolatedValue = interpolation1(lView, prefix, v0, suffix);
if (interpolatedValue !== NO_CHANGE) {
elementAttributeInternal(
getSelectedIndex(), attrName, interpolatedValue, lView, sanitizer, namespace);
const nodeIndex = getSelectedIndex();
elementAttributeInternal(nodeIndex, attrName, interpolatedValue, lView, sanitizer, namespace);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, 'attr.' + attrName, getBindingIndex() - 1,
prefix, suffix);
}
return ɵɵattributeInterpolate1;
}
@ -82,8 +86,11 @@ export function ɵɵattributeInterpolate2(
const lView = getLView();
const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix);
if (interpolatedValue !== NO_CHANGE) {
elementAttributeInternal(
getSelectedIndex(), attrName, interpolatedValue, lView, sanitizer, namespace);
const nodeIndex = getSelectedIndex();
elementAttributeInternal(nodeIndex, attrName, interpolatedValue, lView, sanitizer, namespace);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, 'attr.' + attrName, getBindingIndex() - 2,
prefix, i0, suffix);
}
return ɵɵattributeInterpolate2;
}
@ -123,8 +130,11 @@ export function ɵɵattributeInterpolate3(
const lView = getLView();
const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix);
if (interpolatedValue !== NO_CHANGE) {
elementAttributeInternal(
getSelectedIndex(), attrName, interpolatedValue, lView, sanitizer, namespace);
const nodeIndex = getSelectedIndex();
elementAttributeInternal(nodeIndex, attrName, interpolatedValue, lView, sanitizer, namespace);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, 'attr.' + attrName, getBindingIndex() - 3,
prefix, i0, i1, suffix);
}
return ɵɵattributeInterpolate3;
}
@ -167,8 +177,11 @@ export function ɵɵattributeInterpolate4(
const lView = getLView();
const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix);
if (interpolatedValue !== NO_CHANGE) {
elementAttributeInternal(
getSelectedIndex(), attrName, interpolatedValue, lView, sanitizer, namespace);
const nodeIndex = getSelectedIndex();
elementAttributeInternal(nodeIndex, attrName, interpolatedValue, lView, sanitizer, namespace);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, 'attr.' + attrName, getBindingIndex() - 4,
prefix, i0, i1, i2, suffix);
}
return ɵɵattributeInterpolate4;
}
@ -214,8 +227,11 @@ export function ɵɵattributeInterpolate5(
const interpolatedValue =
interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix);
if (interpolatedValue !== NO_CHANGE) {
elementAttributeInternal(
getSelectedIndex(), attrName, interpolatedValue, lView, sanitizer, namespace);
const nodeIndex = getSelectedIndex();
elementAttributeInternal(nodeIndex, attrName, interpolatedValue, lView, sanitizer, namespace);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, 'attr.' + attrName, getBindingIndex() - 5,
prefix, i0, i1, i2, i3, suffix);
}
return ɵɵattributeInterpolate5;
}
@ -263,8 +279,11 @@ export function ɵɵattributeInterpolate6(
const interpolatedValue =
interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix);
if (interpolatedValue !== NO_CHANGE) {
elementAttributeInternal(
getSelectedIndex(), attrName, interpolatedValue, lView, sanitizer, namespace);
const nodeIndex = getSelectedIndex();
elementAttributeInternal(nodeIndex, attrName, interpolatedValue, lView, sanitizer, namespace);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, 'attr.' + attrName, getBindingIndex() - 6,
prefix, i0, i1, i2, i3, i4, suffix);
}
return ɵɵattributeInterpolate6;
}
@ -310,12 +329,15 @@ export function ɵɵattributeInterpolate7(
attrName: 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, namespace?: string): typeof ɵɵattributeInterpolate7 {
const index = getSelectedIndex();
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) {
elementAttributeInternal(index, attrName, interpolatedValue, lView, sanitizer, namespace);
const nodeIndex = getSelectedIndex();
elementAttributeInternal(nodeIndex, attrName, interpolatedValue, lView, sanitizer, namespace);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, 'attr.' + attrName, getBindingIndex() - 7,
prefix, i0, i1, i2, i3, i4, i5, suffix);
}
return ɵɵattributeInterpolate7;
}
@ -367,8 +389,11 @@ export function ɵɵattributeInterpolate8(
const interpolatedValue = interpolation8(
lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix);
if (interpolatedValue !== NO_CHANGE) {
elementAttributeInternal(
getSelectedIndex(), attrName, interpolatedValue, lView, sanitizer, namespace);
const nodeIndex = getSelectedIndex();
elementAttributeInternal(nodeIndex, attrName, interpolatedValue, lView, sanitizer, namespace);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, 'attr.' + attrName, getBindingIndex() - 8,
prefix, i0, i1, i2, i3, i4, i5, i6, suffix);
}
return ɵɵattributeInterpolate8;
}
@ -405,8 +430,17 @@ export function ɵɵattributeInterpolateV(
const lView = getLView();
const interpolated = interpolationV(lView, values);
if (interpolated !== NO_CHANGE) {
elementAttributeInternal(
getSelectedIndex(), attrName, interpolated, lView, sanitizer, namespace);
const nodeIndex = getSelectedIndex();
elementAttributeInternal(nodeIndex, attrName, interpolated, lView, sanitizer, namespace);
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, 'attr.' + attrName,
getBindingIndex() - interpolationInBetween.length + 1, ...interpolationInBetween);
}
}
return ɵɵattributeInterpolateV;
}

View File

@ -85,10 +85,10 @@ export function ɵɵpropertyInterpolate1(
const lView = getLView();
const interpolatedValue = interpolation1(lView, prefix, v0, suffix);
if (interpolatedValue !== NO_CHANGE) {
elementPropertyInternal(lView, getSelectedIndex(), propName, interpolatedValue, sanitizer);
ngDevMode &&
storePropertyBindingMetadata(
lView[TVIEW].data, getSelectedIndex(), propName, getBindingIndex() - 1, prefix, suffix);
const nodeIndex = getSelectedIndex();
elementPropertyInternal(lView, nodeIndex, propName, interpolatedValue, sanitizer);
ngDevMode && storePropertyBindingMetadata(
lView[TVIEW].data, nodeIndex, propName, getBindingIndex() - 1, prefix, suffix);
}
return ɵɵpropertyInterpolate1;
}

View File

@ -1301,17 +1301,16 @@ describe('change detection', () => {
});
it('should include field name in case of attribute binding', () => {
// TODO(akushnir): improve error message and include attr name in Ivy
const message = ivyEnabled ? `Previous value: 'initial'. Current value: 'changed'` :
`Previous value: 'id: initial'. Current value: 'id: changed'`;
const message = ivyEnabled ?
`Previous value for 'attr.id': 'initial'. Current value: 'changed'` :
`Previous value: 'id: initial'. Current value: 'id: changed'`;
expect(() => initWithTemplate('<div [attr.id]="unstableStringExpression"></div>'))
.toThrowError(new RegExp(message));
});
it('should include field name in case of attribute interpolation', () => {
// TODO(akushnir): improve error message and include attr name and entire expression in Ivy
const message = ivyEnabled ?
`Previous value: 'initial'. Current value: 'changed'` :
`Previous value for 'attr.id': 'Expressions: a and initial!'. Current value: 'Expressions: a and changed!'` :
`Previous value: 'id: Expressions: a and initial!'. Current value: 'id: Expressions: a and changed!'`;
expect(
() => initWithTemplate(