From 873750609f4c81f14bfe061e0e211ea85daa7eb5 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Fri, 18 Jan 2019 14:16:41 +0100 Subject: [PATCH] fix(ivy): calling ChangeDetectorRef.detectChanges() from onChanges should not go infinite loop (#28239) PR Close #28239 --- packages/core/src/render3/instructions.ts | 3 + .../test/render3/change_detection_spec.ts | 60 ++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index f7bdef173c..2caf10d34f 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -68,6 +68,9 @@ export function refreshDescendantViews(lView: LView) { tView.firstTemplatePass = false; setFirstTemplatePass(false); + // Resetting the bindingIndex of the current LView as the next steps may trigger change detection. + lView[BINDING_INDEX] = tView.bindingStartIndex; + // If this is a creation pass, we should not call lifecycle hooks or evaluate bindings. // This will be done in the update pass. if (!isCreationMode(lView)) { diff --git a/packages/core/test/render3/change_detection_spec.ts b/packages/core/test/render3/change_detection_spec.ts index 3f65e9fc25..1a7909caf7 100644 --- a/packages/core/test/render3/change_detection_spec.ts +++ b/packages/core/test/render3/change_detection_spec.ts @@ -11,7 +11,7 @@ import {withBody} from '@angular/private/testing'; import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck, RendererType2} from '../../src/core'; import {whenRendered} from '../../src/render3/component'; -import {LifecycleHooksFeature, defineComponent, defineDirective, getRenderedText, templateRefExtractor} from '../../src/render3/index'; +import {LifecycleHooksFeature, NgOnChangesFeature, defineComponent, defineDirective, getRenderedText, templateRefExtractor} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, detectChanges, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, listener, markDirty, reference, text, template, textBinding, tick} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; @@ -562,6 +562,64 @@ describe('change detection', () => { expect(getRenderedText(comp)).toEqual('1'); }); + + it('should not go infinite loop when recursively called from children\'s ngOnChanges', () => { + class ChildComp { + // @Input + inp = ''; + + count = 0; + constructor(public parentComp: ParentComp) {} + + ngOnChanges() { + this.count++; + if (this.count > 1) throw new Error(`ngOnChanges should be called only once!`); + this.parentComp.triggerChangeDetection(); + } + + static ngComponentDef = defineComponent({ + type: ChildComp, + selectors: [['child-comp']], + factory: () => new ChildComp(directiveInject(ParentComp as any)), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: ChildComp) => { + if (rf & RenderFlags.Create) { + text(0, 'foo'); + } + }, + inputs: {inp: 'inp'}, + features: [NgOnChangesFeature] + }); + } + + class ParentComp { + constructor(public cdr: ChangeDetectorRef) {} + + triggerChangeDetection() { this.cdr.detectChanges(); } + + static ngComponentDef = defineComponent({ + type: ParentComp, + selectors: [['parent-comp']], + factory: () => new ParentComp(directiveInject(ChangeDetectorRef as any)), + consts: 1, + vars: 1, + /** {{ value }} */ + template: (rf: RenderFlags, ctx: ParentComp) => { + if (rf & RenderFlags.Create) { + element(0, 'child-comp'); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'inp', bind(true)); + } + }, + directives: [ChildComp] + }); + } + + expect(() => renderComponent(ParentComp)).not.toThrow(); + }); + it('should support call in ngDoCheck', () => { class DetectChangesComp { doCheckCount = 0;