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;