fix(ivy): calling ChangeDetectorRef.detectChanges() from onChanges should not go infinite loop (#28239)

PR Close #28239
This commit is contained in:
Marc Laval 2019-01-18 14:16:41 +01:00 committed by Jason Aden
parent 664ea50b46
commit 873750609f
2 changed files with 62 additions and 1 deletions

View File

@ -68,6 +68,9 @@ export function refreshDescendantViews(lView: LView) {
tView.firstTemplatePass = false; tView.firstTemplatePass = false;
setFirstTemplatePass(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. // If this is a creation pass, we should not call lifecycle hooks or evaluate bindings.
// This will be done in the update pass. // This will be done in the update pass.
if (!isCreationMode(lView)) { if (!isCreationMode(lView)) {

View File

@ -11,7 +11,7 @@ import {withBody} from '@angular/private/testing';
import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck, RendererType2} from '../../src/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck, RendererType2} from '../../src/core';
import {whenRendered} from '../../src/render3/component'; 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 {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'; import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -562,6 +562,64 @@ describe('change detection', () => {
expect(getRenderedText(comp)).toEqual('1'); 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', () => { it('should support call in ngDoCheck', () => {
class DetectChangesComp { class DetectChangesComp {
doCheckCount = 0; doCheckCount = 0;