diff --git a/packages/core/src/view/util.ts b/packages/core/src/view/util.ts index 90d1580c2a..eade00199e 100644 --- a/packages/core/src/view/util.ts +++ b/packages/core/src/view/util.ts @@ -98,7 +98,7 @@ export function checkBindingNoChanges( view: ViewData, def: NodeDef, bindingIdx: number, value: any) { const oldValue = view.oldValues[def.bindingIndex + bindingIdx]; if ((view.state & ViewState.BeforeFirstCheck) || !devModeEqual(oldValue, value)) { - const bindingName = def.bindings[def.bindingIndex].name; + const bindingName = def.bindings[bindingIdx].name; throw expressionChangedAfterItHasBeenCheckedError( Services.createDebugContext(view, def.nodeIndex), `${bindingName}: ${oldValue}`, `${bindingName}: ${value}`, (view.state & ViewState.BeforeFirstCheck) !== 0); diff --git a/packages/core/test/linker/change_detection_integration_spec.ts b/packages/core/test/linker/change_detection_integration_spec.ts index 1d4818bd30..138438d065 100644 --- a/packages/core/test/linker/change_detection_integration_spec.ts +++ b/packages/core/test/linker/change_detection_integration_spec.ts @@ -1156,11 +1156,19 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ describe('enforce no new changes', () => { it('should throw when a record gets changed after it has been checked', fakeAsync(() => { - const ctx = createCompFixture('
', TestData); - ctx.componentInstance.a = 1; + @Directive({selector: '[changed]'}) + class ChangingDirective { + @Input() changed: any; + } + + TestBed.configureTestingModule({declarations: [ChangingDirective]}); + + const ctx = createCompFixture('', TestData); + + ctx.componentInstance.b = 1; expect(() => ctx.checkNoChanges()) - .toThrowError(/Expression has changed after it was checked./g); + .toThrowError(/Previous value: 'changed: undefined'\. Current value: 'changed: 1'/g); })); it('should warn when the view has been created in a cd hook', fakeAsync(() => { @@ -1968,7 +1976,8 @@ class Uninitialized { @Component({selector: 'root', template: 'empty'}) class TestData { - public a: any; + a: any; + b: any; } @Component({selector: 'root', template: 'empty'}) diff --git a/packages/core/test/view/component_view_spec.ts b/packages/core/test/view/component_view_spec.ts index 3f073ec7ff..04b30d681d 100644 --- a/packages/core/test/view/component_view_spec.ts +++ b/packages/core/test/view/component_view_spec.ts @@ -135,6 +135,37 @@ const addEventListener = '__zone_symbol__addEventListener'; `ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'a: v1'. Current value: 'a: v2'.`); }); + // fixes https://github.com/angular/angular/issues/21788 + it('report the binding name when an expression changes after it has been checked', () => { + let value: any; + class AComp {} + + const update = + jasmine.createSpy('updater').and.callFake((check: NodeCheckFn, view: ViewData) => { + check(view, 0, ArgumentType.Inline, 'const', 'const', value); + }); + + const {view, rootNodes} = createAndGetRootNodes( + compViewDef([ + elementDef(0, NodeFlags.None, null, null, 1, 'div', null, null, null, null, () => compViewDef([ + elementDef(0, NodeFlags.None, null, null, 0, 'span', null, [ + [BindingFlags.TypeElementAttribute, 'p1', SecurityContext.NONE], + [BindingFlags.TypeElementAttribute, 'p2', SecurityContext.NONE], + [BindingFlags.TypeElementAttribute, 'p3', SecurityContext.NONE], + ]), + ], null, update) + ), + directiveDef(1, NodeFlags.Component, null, 0, AComp, []), + ])); + + value = 'v1'; + Services.checkAndUpdateView(view); + value = 'v2'; + expect(() => Services.checkNoChangesView(view)) + .toThrowError( + `ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'p3: v1'. Current value: 'p3: v2'.`); + }); + it('should support detaching and attaching component views for dirty checking', () => { class AComp { a: any;