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:
parent
181d766941
commit
c3f14bb7a5
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'` :
|
||||
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(
|
||||
|
|
Loading…
Reference in New Issue