diff --git a/modules/@angular/core/src/change_detection/change_detection_util.ts b/modules/@angular/core/src/change_detection/change_detection_util.ts index 1b5af4cf51..cd9b6c852f 100644 --- a/modules/@angular/core/src/change_detection/change_detection_util.ts +++ b/modules/@angular/core/src/change_detection/change_detection_util.ts @@ -11,7 +11,9 @@ import {isPrimitive, looseIdentical} from '../facade/lang'; export {looseIdentical} from '../facade/lang'; -export const UNINITIALIZED = new Object(); +export const UNINITIALIZED = { + toString: () => 'CD_INIT_VALUE' +}; export function devModeEqual(a: any, b: any): boolean { if (isListLikeIterable(a) && isListLikeIterable(b)) { diff --git a/modules/@angular/core/src/linker/exceptions.ts b/modules/@angular/core/src/linker/exceptions.ts index 36d84b2688..049af39779 100644 --- a/modules/@angular/core/src/linker/exceptions.ts +++ b/modules/@angular/core/src/linker/exceptions.ts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ +import {UNINITIALIZED} from '../change_detection/change_detection_util'; import {BaseException, WrappedException} from '../facade/exceptions'; + /** * An error thrown if application changes model breaking the top-down data flow. * @@ -44,9 +46,14 @@ import {BaseException, WrappedException} from '../facade/exceptions'; */ export class ExpressionChangedAfterItHasBeenCheckedException extends BaseException { constructor(oldValue: any, currValue: any, context: any) { - super( - `Expression has changed after it was checked. ` + - `Previous value: '${oldValue}'. Current value: '${currValue}'`); + let msg = + `Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`; + if (oldValue === UNINITIALIZED) { + 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 ?`; + } + super(msg); } } diff --git a/modules/@angular/core/test/linker/change_detection_integration_spec.ts b/modules/@angular/core/test/linker/change_detection_integration_spec.ts index d8f2524d95..987e666c95 100644 --- a/modules/@angular/core/test/linker/change_detection_integration_spec.ts +++ b/modules/@angular/core/test/linker/change_detection_integration_spec.ts @@ -1002,15 +1002,22 @@ export function main() { describe('enforce no new changes', () => { it('should throw when a record gets changed after it has been checked', fakeAsync(() => { - var ctx = createCompFixture('
', TestData); - + const ctx = createCompFixture('
', TestData); ctx.componentInstance.a = 1; expect(() => ctx.checkNoChanges()) .toThrowError(/:0:5[\s\S]*Expression has changed after it was checked./g); })); + it('should warn when the view has been created in a cd hook', fakeAsync(() => { + const ctx = createCompFixture('
{{ a }}
', TestData); + ctx.componentInstance.a = 1; + expect(() => ctx.detectChanges()) + .toThrowError( + /It seems like the view has been created after its parent and its children have been dirty checked/); + })); + it('should not throw when two arrays are structurally the same', fakeAsync(() => { - var ctx = _bindSimpleValue('a', TestData); + const ctx = _bindSimpleValue('a', TestData); ctx.componentInstance.a = ['value']; ctx.detectChanges(false); ctx.componentInstance.a = ['value']; @@ -1018,9 +1025,8 @@ export function main() { })); it('should not break the next run', fakeAsync(() => { - var ctx = _bindSimpleValue('a', TestData); + const ctx = _bindSimpleValue('a', TestData); ctx.componentInstance.a = 'value'; - expect(() => ctx.checkNoChanges()).toThrow(); ctx.detectChanges(); @@ -1093,17 +1099,28 @@ export function main() { } const ALL_DIRECTIVES = /*@ts2dart_const*/[ - forwardRef(() => TestDirective), forwardRef(() => TestComponent), - forwardRef(() => AnotherComponent), forwardRef(() => TestLocals), forwardRef(() => CompWithRef), - forwardRef(() => EmitterDirective), forwardRef(() => PushComp), - forwardRef(() => OrderCheckDirective2), forwardRef(() => OrderCheckDirective0), - forwardRef(() => OrderCheckDirective1), NgFor + forwardRef(() => TestDirective), + forwardRef(() => TestComponent), + forwardRef(() => AnotherComponent), + forwardRef(() => TestLocals), + forwardRef(() => CompWithRef), + forwardRef(() => EmitterDirective), + forwardRef(() => PushComp), + forwardRef(() => OrderCheckDirective2), + forwardRef(() => OrderCheckDirective0), + forwardRef(() => OrderCheckDirective1), + forwardRef(() => Gh9882), + NgFor, ]; const ALL_PIPES = /*@ts2dart_const*/[ - forwardRef(() => CountingPipe), forwardRef(() => CountingImpurePipe), - forwardRef(() => MultiArgPipe), forwardRef(() => PipeWithOnDestroy), - forwardRef(() => IdentityPipe), forwardRef(() => WrappedPipe), AsyncPipe + forwardRef(() => CountingPipe), + forwardRef(() => CountingImpurePipe), + forwardRef(() => MultiArgPipe), + forwardRef(() => PipeWithOnDestroy), + forwardRef(() => IdentityPipe), + forwardRef(() => WrappedPipe), + AsyncPipe, ]; @Injectable() @@ -1259,7 +1276,7 @@ class EmitterDirective { @Output('event') emitter = new EventEmitter(); } -@Directive({selector: '[gh-9882]'}) +@Directive({selector: '[gh9882]'}) class Gh9882 implements AfterContentInit { constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef) { }