diff --git a/packages/core/src/render3/hooks.ts b/packages/core/src/render3/hooks.ts index b7b41ac026..5199d2846e 100644 --- a/packages/core/src/render3/hooks.ts +++ b/packages/core/src/render3/hooks.ts @@ -212,9 +212,10 @@ function callHooks( (currentView[PREORDER_HOOK_FLAGS] & PreOrderHookFlags.IndexOfTheNextPreOrderHookMaskMask) : 0; const nodeIndexLimit = currentNodeIndex != null ? currentNodeIndex : -1; + const max = arr.length - 1; // Stop the loop at length - 1, because we look for the hook at i + 1 let lastNodeIndexFound = 0; - for (let i = startIndex; i < arr.length; i++) { - const hook = arr[i + 1] as () => void; + for (let i = startIndex; i < max; i++) { + const hook = arr[i + 1] as number | (() => void); if (typeof hook === 'number') { lastNodeIndexFound = arr[i] as number; if (currentNodeIndex != null && lastNodeIndexFound >= currentNodeIndex) { @@ -250,8 +251,7 @@ function callHook(currentView: LView, initPhase: InitPhaseState, arr: HookData, const directive = currentView[directiveIndex]; if (isInitHook) { const indexWithintInitPhase = currentView[FLAGS] >> LViewFlags.IndexWithinInitPhaseShift; - // The init phase state must be always checked here as it may have been recursively - // updated + // The init phase state must be always checked here as it may have been recursively updated. if (indexWithintInitPhase < (currentView[PREORDER_HOOK_FLAGS] >> PreOrderHookFlags.NumberOfInitHooksCalledShift) && (currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase) { diff --git a/packages/core/test/acceptance/lifecycle_spec.ts b/packages/core/test/acceptance/lifecycle_spec.ts index d0ab6fec68..e322a78aeb 100644 --- a/packages/core/test/acceptance/lifecycle_spec.ts +++ b/packages/core/test/acceptance/lifecycle_spec.ts @@ -7,8 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {ChangeDetectorRef, Component, ComponentFactoryResolver, ContentChildren, Directive, Input, NgModule, OnChanges, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; -import {SimpleChange} from '@angular/core/src/core'; +import {AfterViewInit, ChangeDetectorRef, Component, ComponentFactoryResolver, ContentChildren, Directive, DoCheck, Input, NgModule, OnChanges, QueryList, SimpleChange, SimpleChanges, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {onlyInIvy} from '@angular/private/testing'; @@ -4378,4 +4377,72 @@ describe('non-regression', () => { expect(destroyed).toBeTruthy(); }); + + onlyInIvy('Use case is not supported in ViewEngine') + .it('should not throw when calling detectChanges from a setter in the presence of a data binding, ngOnChanges and ngAfterViewInit', + () => { + const hooks: string[] = []; + + @Directive({selector: '[testDir]'}) + class TestDirective implements OnChanges, AfterViewInit { + constructor(private _changeDetectorRef: ChangeDetectorRef) {} + + @Input('testDir') + set value(_value: any) { + this._changeDetectorRef.detectChanges(); + } + ngOnChanges() { + hooks.push('ngOnChanges'); + } + ngAfterViewInit() { + hooks.push('ngAfterViewInit'); + } + } + + @Component({template: `
{{value}}
`}) + class App { + value = 1; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + expect(() => fixture.detectChanges()).not.toThrow(); + expect(hooks).toEqual(['ngOnChanges', 'ngAfterViewInit']); + expect(fixture.nativeElement.textContent.trim()).toBe('1'); + }); + + onlyInIvy('Use case is not supported in ViewEngine') + .it('should call hooks in the correct order when calling detectChanges in a setter', () => { + const hooks: string[] = []; + + @Directive({selector: '[testDir]'}) + class TestDirective implements OnChanges, DoCheck, AfterViewInit { + constructor(private _changeDetectorRef: ChangeDetectorRef) {} + + @Input('testDir') + set value(_value: any) { + this._changeDetectorRef.detectChanges(); + } + ngOnChanges() { + hooks.push('ngOnChanges'); + } + ngDoCheck() { + hooks.push('ngDoCheck'); + } + ngAfterViewInit() { + hooks.push('ngAfterViewInit'); + } + } + + @Component({template: `
{{value}}
`}) + class App { + value = 1; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + expect(() => fixture.detectChanges()).not.toThrow(); + expect(hooks).toEqual(['ngOnChanges', 'ngDoCheck', 'ngAfterViewInit']); + expect(fixture.nativeElement.textContent.trim()).toBe('1'); + }); });