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
		
			
				
	
	
		
			117 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			117 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 
 | ||
| /**
 | ||
|  * @license
 | ||
|  * Copyright Google Inc. All Rights Reserved.
 | ||
|  *
 | ||
|  * 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 {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) */
 | ||
| export function throwCyclicDependencyError(token: any): never {
 | ||
|   throw new Error(`Cannot instantiate cyclic dependency! ${token}`);
 | ||
| }
 | ||
| 
 | ||
| /** Called when there are multiple component selectors that match a given node */
 | ||
| export function throwMultipleComponentError(tNode: TNode): never {
 | ||
|   throw new Error(`Multiple components match node with tagname ${tNode.tagName}`);
 | ||
| }
 | ||
| 
 | ||
| export function throwMixedMultiProviderError() {
 | ||
|   throw new Error(`Cannot mix multi providers and regular providers`);
 | ||
| }
 | ||
| 
 | ||
| export function throwInvalidProviderError(
 | ||
|     ngModuleType?: InjectorType<any>, providers?: any[], provider?: any) {
 | ||
|   let ngModuleDetail = '';
 | ||
|   if (ngModuleType && providers) {
 | ||
|     const providerDetail = providers.map(v => v == provider ? '?' + provider + '?' : '...');
 | ||
|     ngModuleDetail =
 | ||
|         ` - only instances of Provider and Type are allowed, got: [${providerDetail.join(', ')}]`;
 | ||
|   }
 | ||
| 
 | ||
|   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};
 | ||
| } |