fix(ivy): improve ExpressionChangedAfterChecked error (#34381)
Prior to this change, the ExpressionChangedAfterChecked error thrown in Ivy was missing useful information that was available in View Engine, specifically: missing property name for proprty bindings and also the content of the entire property interpolation (only a changed value was displayed) if one of expressions was changed unexpectedly. This commit improves the error message by including the mentioned information into the error text. PR Close #34381
This commit is contained in:

committed by
Kara Erickson

parent
82dce68e13
commit
7ea39849ff
@ -10,6 +10,9 @@ import {InjectorType} from '../di/interface/defs';
|
||||
import {stringify} from '../util/stringify';
|
||||
|
||||
import {TNode} from './interfaces/node';
|
||||
import {LView, TVIEW} from './interfaces/view';
|
||||
import {INTERPOLATION_DELIMITER} from './util/misc_utils';
|
||||
|
||||
|
||||
|
||||
/** Called when directives inject each other (creating a circular dependency) */
|
||||
@ -22,20 +25,6 @@ export function throwMultipleComponentError(tNode: TNode): never {
|
||||
throw new Error(`Multiple components match node with tagname ${tNode.tagName}`);
|
||||
}
|
||||
|
||||
/** Throws an ExpressionChangedAfterChecked error if checkNoChanges mode is on. */
|
||||
export function throwErrorIfNoChangesMode(
|
||||
creationMode: boolean, oldValue: any, currValue: any): never|void {
|
||||
let msg =
|
||||
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`;
|
||||
if (creationMode) {
|
||||
msg +=
|
||||
` It seems like the view has been created after its parent and its children have been dirty checked.` +
|
||||
` Has it been created in a change detection hook ?`;
|
||||
}
|
||||
// TODO: include debug context
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
export function throwMixedMultiProviderError() {
|
||||
throw new Error(`Cannot mix multi providers and regular providers`);
|
||||
}
|
||||
@ -52,3 +41,77 @@ export function throwInvalidProviderError(
|
||||
throw new Error(
|
||||
`Invalid provider for the NgModule '${stringify(ngModuleType)}'` + ngModuleDetail);
|
||||
}
|
||||
|
||||
/** Throws an ExpressionChangedAfterChecked error if checkNoChanges mode is on. */
|
||||
export function throwErrorIfNoChangesMode(
|
||||
creationMode: boolean, oldValue: any, currValue: any, propName?: string): never|void {
|
||||
const field = propName ? ` for '${propName}'` : '';
|
||||
let msg =
|
||||
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value${field}: '${oldValue}'. Current value: '${currValue}'.`;
|
||||
if (creationMode) {
|
||||
msg +=
|
||||
` It seems like the view has been created after its parent and its children have been dirty checked.` +
|
||||
` Has it been created in a change detection hook?`;
|
||||
}
|
||||
// TODO: include debug context, see `viewDebugError` function in
|
||||
// `packages/core/src/view/errors.ts` for reference.
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
function constructDetailsForInterpolation(
|
||||
lView: LView, rootIndex: number, expressionIndex: number, meta: string, changedValue: any) {
|
||||
const [propName, prefix, ...chunks] = meta.split(INTERPOLATION_DELIMITER);
|
||||
let oldValue = prefix, newValue = prefix;
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const slotIdx = rootIndex + i;
|
||||
oldValue += `${lView[slotIdx]}${chunks[i]}`;
|
||||
newValue += `${slotIdx === expressionIndex ? changedValue : lView[slotIdx]}${chunks[i]}`;
|
||||
}
|
||||
return {propName, oldValue, newValue};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an object that contains details for the ExpressionChangedAfterItHasBeenCheckedError:
|
||||
* - property name (for property bindings or interpolations)
|
||||
* - old and new values, enriched using information from metadata
|
||||
*
|
||||
* More information on the metadata storage format can be found in `storePropertyBindingMetadata`
|
||||
* function description.
|
||||
*/
|
||||
export function getExpressionChangedErrorDetails(
|
||||
lView: LView, bindingIndex: number, oldValue: any,
|
||||
newValue: any): {propName?: string, oldValue: any, newValue: any} {
|
||||
const tData = lView[TVIEW].data;
|
||||
const metadata = tData[bindingIndex];
|
||||
|
||||
if (typeof metadata === 'string') {
|
||||
// metadata for property interpolation
|
||||
if (metadata.indexOf(INTERPOLATION_DELIMITER) > -1) {
|
||||
return constructDetailsForInterpolation(
|
||||
lView, bindingIndex, bindingIndex, metadata, newValue);
|
||||
}
|
||||
// metadata for property binding
|
||||
return {propName: metadata, oldValue, newValue};
|
||||
}
|
||||
|
||||
// metadata is not available for this expression, check if this expression is a part of the
|
||||
// property interpolation by going from the current binding index left and look for a string that
|
||||
// contains INTERPOLATION_DELIMITER, the layout in tView.data for this case will look like this:
|
||||
// [..., 'id<69>Prefix <20> and <20> suffix', null, null, null, ...]
|
||||
if (metadata === null) {
|
||||
let idx = bindingIndex - 1;
|
||||
while (typeof tData[idx] !== 'string' && tData[idx + 1] === null) {
|
||||
idx--;
|
||||
}
|
||||
const meta = tData[idx];
|
||||
if (typeof meta === 'string') {
|
||||
const matches = meta.match(new RegExp(INTERPOLATION_DELIMITER, 'g'));
|
||||
// first interpolation delimiter separates property name from interpolation parts (in case of
|
||||
// property interpolations), so we subtract one from total number of found delimiters
|
||||
if (matches && (matches.length - 1) > bindingIndex - idx) {
|
||||
return constructDetailsForInterpolation(lView, idx, bindingIndex, meta, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {propName: undefined, oldValue, newValue};
|
||||
}
|
Reference in New Issue
Block a user