fix(ivy): init hooks should be called once and only once (#28239)

PR Close #28239
This commit is contained in:
Marc Laval 2019-01-18 17:38:39 +01:00 committed by Jason Aden
parent 873750609f
commit d83307adab
9 changed files with 233 additions and 184 deletions

View File

@ -256,6 +256,7 @@ export function defineComponent<T>(componentDefinition: {
inputs: null !, // assigned in noSideEffects inputs: null !, // assigned in noSideEffects
outputs: null !, // assigned in noSideEffects outputs: null !, // assigned in noSideEffects
exportAs: componentDefinition.exportAs || null, exportAs: componentDefinition.exportAs || null,
onChanges: null,
onInit: typePrototype.ngOnInit || null, onInit: typePrototype.ngOnInit || null,
doCheck: typePrototype.ngDoCheck || null, doCheck: typePrototype.ngDoCheck || null,
afterContentInit: typePrototype.ngAfterContentInit || null, afterContentInit: typePrototype.ngAfterContentInit || null,

View File

@ -49,16 +49,11 @@ export function NgOnChangesFeature<T>(): DirectiveDefFeature {
function NgOnChangesFeatureImpl<T>(definition: DirectiveDef<T>): void { function NgOnChangesFeatureImpl<T>(definition: DirectiveDef<T>): void {
if (definition.type.prototype.ngOnChanges) { if (definition.type.prototype.ngOnChanges) {
definition.setInput = ngOnChangesSetInput; definition.setInput = ngOnChangesSetInput;
definition.onChanges = wrapOnChanges();
const prevDoCheck = definition.doCheck;
const prevOnInit = definition.onInit;
definition.onInit = wrapOnChanges(prevOnInit);
definition.doCheck = wrapOnChanges(prevDoCheck);
} }
} }
function wrapOnChanges(hook: (() => void) | null) { function wrapOnChanges() {
return function(this: OnChanges) { return function(this: OnChanges) {
const simpleChangesStore = getSimpleChangesStore(this); const simpleChangesStore = getSimpleChangesStore(this);
const current = simpleChangesStore && simpleChangesStore.current; const current = simpleChangesStore && simpleChangesStore.current;
@ -68,8 +63,6 @@ function wrapOnChanges(hook: (() => void) | null) {
simpleChangesStore !.current = null; simpleChangesStore !.current = null;
this.ngOnChanges(current); this.ngOnChanges(current);
} }
hook && hook.call(this);
}; };
} }

View File

@ -6,12 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {SimpleChanges} from '../interface/simple_change';
import {assertEqual} from '../util/assert'; import {assertEqual} from '../util/assert';
import {DirectiveDef} from './interfaces/definition'; import {DirectiveDef} from './interfaces/definition';
import {TNode} from './interfaces/node'; import {TNode} from './interfaces/node';
import {FLAGS, HookData, LView, LViewFlags, TView} from './interfaces/view'; import {FLAGS, HookData, InitPhaseState, LView, LViewFlags, TView} from './interfaces/view';
@ -34,10 +33,15 @@ export function registerPreOrderHooks(
ngDevMode && ngDevMode &&
assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass'); assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass');
const {onInit, doCheck} = directiveDef; const {onChanges, onInit, doCheck} = directiveDef;
if (onChanges) {
(tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onChanges);
(tView.checkHooks || (tView.checkHooks = [])).push(directiveIndex, onChanges);
}
if (onInit) { if (onInit) {
(tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onInit); (tView.initHooks || (tView.initHooks = [])).push(-directiveIndex, onInit);
} }
if (doCheck) { if (doCheck) {
@ -73,7 +77,7 @@ export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
for (let i = tNode.directiveStart, end = tNode.directiveEnd; i < end; i++) { for (let i = tNode.directiveStart, end = tNode.directiveEnd; i < end; i++) {
const directiveDef = tView.data[i] as DirectiveDef<any>; const directiveDef = tView.data[i] as DirectiveDef<any>;
if (directiveDef.afterContentInit) { if (directiveDef.afterContentInit) {
(tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentInit); (tView.contentHooks || (tView.contentHooks = [])).push(-i, directiveDef.afterContentInit);
} }
if (directiveDef.afterContentChecked) { if (directiveDef.afterContentChecked) {
@ -83,7 +87,7 @@ export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
} }
if (directiveDef.afterViewInit) { if (directiveDef.afterViewInit) {
(tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewInit); (tView.viewHooks || (tView.viewHooks = [])).push(-i, directiveDef.afterViewInit);
} }
if (directiveDef.afterViewChecked) { if (directiveDef.afterViewChecked) {
@ -114,9 +118,10 @@ export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
*/ */
export function executeInitHooks( export function executeInitHooks(
currentView: LView, tView: TView, checkNoChangesMode: boolean): void { currentView: LView, tView: TView, checkNoChangesMode: boolean): void {
if (!checkNoChangesMode && currentView[FLAGS] & LViewFlags.RunInit) { if (!checkNoChangesMode) {
executeHooks(currentView, tView.initHooks, tView.checkHooks, checkNoChangesMode); executeHooks(
currentView[FLAGS] &= ~LViewFlags.RunInit; currentView, tView.initHooks, tView.checkHooks, checkNoChangesMode,
InitPhaseState.OnInitHooksToBeRun);
} }
} }
@ -131,12 +136,19 @@ export function executeInitHooks(
*/ */
export function executeHooks( export function executeHooks(
currentView: LView, firstPassHooks: HookData | null, checkHooks: HookData | null, currentView: LView, firstPassHooks: HookData | null, checkHooks: HookData | null,
checkNoChangesMode: boolean): void { checkNoChangesMode: boolean, initPhase: number): void {
if (checkNoChangesMode) return; if (checkNoChangesMode) return;
const hooksToCall = (currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase ?
const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? firstPassHooks : checkHooks; firstPassHooks :
checkHooks;
if (hooksToCall) { if (hooksToCall) {
callHooks(currentView, hooksToCall); callHooks(currentView, hooksToCall, initPhase);
}
// The init phase state must be always checked here as it may have been recursively updated
if ((currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase &&
initPhase !== InitPhaseState.InitPhaseCompleted) {
currentView[FLAGS] &= LViewFlags.IndexWithinInitPhaseReset;
currentView[FLAGS] += LViewFlags.InitPhaseStateIncrementer;
} }
} }
@ -147,8 +159,24 @@ export function executeHooks(
* @param currentView The current view * @param currentView The current view
* @param arr The array in which the hooks are found * @param arr The array in which the hooks are found
*/ */
export function callHooks(currentView: LView, arr: HookData): void { export function callHooks(currentView: LView, arr: HookData, initPhase?: number): void {
let initHooksCount = 0;
for (let i = 0; i < arr.length; i += 2) { for (let i = 0; i < arr.length; i += 2) {
(arr[i + 1] as() => void).call(currentView[arr[i] as number]); const isInitHook = arr[i] < 0;
const directiveIndex = isInitHook ? -arr[i] : arr[i] as number;
const directive = currentView[directiveIndex];
const hook = arr[i + 1] as() => void;
if (isInitHook) {
initHooksCount++;
const indexWithintInitPhase = currentView[FLAGS] >> LViewFlags.IndexWithinInitPhaseShift;
// The init phase state must be always checked here as it may have been recursively updated
if (indexWithintInitPhase < initHooksCount &&
(currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase) {
currentView[FLAGS] += LViewFlags.IndexWithinInitPhaseIncrementer;
hook.call(directive);
}
} else {
hook.call(directive);
}
} }
} }

View File

@ -32,7 +32,7 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'
import {LQueries} from './interfaces/query'; import {LQueries} from './interfaces/query';
import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer'; import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization'; import {SanitizerFn} from './interfaces/sanitization';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view'; import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation'; import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
@ -83,7 +83,9 @@ export function refreshDescendantViews(lView: LView) {
// Content query results must be refreshed before content hooks are called. // Content query results must be refreshed before content hooks are called.
refreshContentQueries(tView); refreshContentQueries(tView);
executeHooks(lView, tView.contentHooks, tView.contentCheckHooks, checkNoChangesMode); executeHooks(
lView, tView.contentHooks, tView.contentCheckHooks, checkNoChangesMode,
InitPhaseState.AfterContentInitHooksToBeRun);
setHostBindings(tView, lView); setHostBindings(tView, lView);
} }
@ -159,8 +161,7 @@ export function createLView<T>(
rendererFactory?: RendererFactory3 | null, renderer?: Renderer3 | null, rendererFactory?: RendererFactory3 | null, renderer?: Renderer3 | null,
sanitizer?: Sanitizer | null, injector?: Injector | null): LView { sanitizer?: Sanitizer | null, injector?: Injector | null): LView {
const lView = tView.blueprint.slice() as LView; const lView = tView.blueprint.slice() as LView;
lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit | lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.FirstLViewPass;
LViewFlags.FirstLViewPass;
lView[PARENT] = lView[DECLARATION_VIEW] = parentLView; lView[PARENT] = lView[DECLARATION_VIEW] = parentLView;
lView[CONTEXT] = context; lView[CONTEXT] = context;
lView[RENDERER_FACTORY] = (rendererFactory || parentLView && parentLView[RENDERER_FACTORY]) !; lView[RENDERER_FACTORY] = (rendererFactory || parentLView && parentLView[RENDERER_FACTORY]) !;

View File

@ -140,6 +140,7 @@ export interface DirectiveDef<T> extends BaseDef<T> {
hostBindings: HostBindingsFunction<T>|null; hostBindings: HostBindingsFunction<T>|null;
/* The following are lifecycle hooks for this component */ /* The following are lifecycle hooks for this component */
onChanges: (() => void)|null;
onInit: (() => void)|null; onInit: (() => void)|null;
doCheck: (() => void)|null; doCheck: (() => void)|null;
afterContentInit: (() => void)|null; afterContentInit: (() => void)|null;

View File

@ -215,6 +215,10 @@ export interface LView extends Array<any> {
/** Flags associated with an LView (saved in LView[FLAGS]) */ /** Flags associated with an LView (saved in LView[FLAGS]) */
export const enum LViewFlags { export const enum LViewFlags {
/** The state of the init phase on the first 2 bits */
InitPhaseStateIncrementer = 0b00000000001,
InitPhaseStateMask = 0b00000000011,
/** /**
* Whether or not the view is in creationMode. * Whether or not the view is in creationMode.
* *
@ -223,7 +227,7 @@ export const enum LViewFlags {
* back into the parent view, `data` will be defined and `creationMode` will be * back into the parent view, `data` will be defined and `creationMode` will be
* improperly reported as false. * improperly reported as false.
*/ */
CreationMode = 0b000000001, CreationMode = 0b00000000100,
/** /**
* Whether or not this LView instance is on its first processing pass. * Whether or not this LView instance is on its first processing pass.
@ -232,31 +236,43 @@ export const enum LViewFlags {
* has completed one creation mode run and one update mode run. At this * has completed one creation mode run and one update mode run. At this
* time, the flag is turned off. * time, the flag is turned off.
*/ */
FirstLViewPass = 0b000000010, FirstLViewPass = 0b00000001000,
/** Whether this view has default change detection strategy (checks always) or onPush */ /** Whether this view has default change detection strategy (checks always) or onPush */
CheckAlways = 0b000000100, CheckAlways = 0b00000010000,
/** Whether or not this view is currently dirty (needing check) */ /** Whether or not this view is currently dirty (needing check) */
Dirty = 0b000001000, Dirty = 0b00000100000,
/** Whether or not this view is currently attached to change detection tree. */ /** Whether or not this view is currently attached to change detection tree. */
Attached = 0b000010000, Attached = 0b00001000000,
/**
* Whether or not the init hooks have run.
*
* If on, the init hooks haven't yet been run and should be executed by the first component that
* runs OR the first cR() instruction that runs (so inits are run for the top level view before
* any embedded views).
*/
RunInit = 0b000100000,
/** Whether or not this view is destroyed. */ /** Whether or not this view is destroyed. */
Destroyed = 0b001000000, Destroyed = 0b00010000000,
/** Whether or not this view is the root view */ /** Whether or not this view is the root view */
IsRoot = 0b010000000, IsRoot = 0b00100000000,
/**
* Index of the current init phase on last 23 bits
*/
IndexWithinInitPhaseIncrementer = 0b01000000000,
IndexWithinInitPhaseShift = 9,
IndexWithinInitPhaseReset = 0b00111111111,
}
/**
* Possible states of the init phase:
* - 00: OnInit hooks to be run.
* - 01: AfterContentInit hooks to be run
* - 10: AfterViewInit hooks to be run
* - 11: All init hooks have been run
*/
export const enum InitPhaseState {
OnInitHooksToBeRun = 0b00,
AfterContentInitHooksToBeRun = 0b01,
AfterViewInitHooksToBeRun = 0b10,
InitPhaseCompleted = 0b11,
} }
/** /**

View File

@ -11,7 +11,7 @@ import {executeHooks} from './hooks';
import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {TElementNode, TNode, TNodeFlags, TViewNode} from './interfaces/node'; import {TElementNode, TNode, TNodeFlags, TViewNode} from './interfaces/node';
import {LQueries} from './interfaces/query'; import {LQueries} from './interfaces/query';
import {BINDING_INDEX, CONTEXT, DECLARATION_VIEW, FLAGS, HOST_NODE, LView, LViewFlags, OpaqueViewState, QUERIES, TVIEW} from './interfaces/view'; import {BINDING_INDEX, CONTEXT, DECLARATION_VIEW, FLAGS, HOST_NODE, InitPhaseState, LView, LViewFlags, OpaqueViewState, QUERIES, TVIEW} from './interfaces/view';
import {isContentQueryHost} from './util'; import {isContentQueryHost} from './util';
@ -336,11 +336,12 @@ export function leaveView(newView: LView): void {
lView[FLAGS] &= ~LViewFlags.CreationMode; lView[FLAGS] &= ~LViewFlags.CreationMode;
} else { } else {
try { try {
executeHooks(lView, tView.viewHooks, tView.viewCheckHooks, checkNoChangesMode); executeHooks(
lView, tView.viewHooks, tView.viewCheckHooks, checkNoChangesMode,
InitPhaseState.AfterViewInitHooksToBeRun);
} finally { } finally {
// Views are clean and in update mode after being checked, so these bits are cleared // Views are clean and in update mode after being checked, so these bits are cleared
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass); lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
lView[FLAGS] |= LViewFlags.RunInit;
lView[BINDING_INDEX] = tView.bindingStartIndex; lView[BINDING_INDEX] = tView.bindingStartIndex;
} }
} }

View File

@ -1588,111 +1588,110 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
childThrows: LifetimeMethods; childThrows: LifetimeMethods;
} }
fixmeIvy('FW-832: View engine supports recursive detectChanges() calls') describe('calling init', () => {
.describe('calling init', () => { function initialize(options: Options) {
function initialize(options: Options) { @Component({selector: 'my-child', template: ''})
@Component({selector: 'my-child', template: ''}) class MyChild {
class MyChild { private thrown = LifetimeMethods.None;
private thrown = LifetimeMethods.None;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() inp !: boolean; @Input() inp !: boolean;
@Output() outp = new EventEmitter<any>(); @Output() outp = new EventEmitter<any>();
constructor() {} constructor() {}
ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); } ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); }
ngOnInit() { this.check(LifetimeMethods.ngOnInit); } ngOnInit() { this.check(LifetimeMethods.ngOnInit); }
ngOnChanges() { this.check(LifetimeMethods.ngOnChanges); } ngOnChanges() { this.check(LifetimeMethods.ngOnChanges); }
ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); } ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); }
ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); } ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); }
private check(method: LifetimeMethods) { private check(method: LifetimeMethods) {
log(`MyChild::${LifetimeMethods[method]}()`); log(`MyChild::${LifetimeMethods[method]}()`);
if ((options.childRecursion & method) !== 0) { if ((options.childRecursion & method) !== 0) {
if (logged.length < 20) { if (logged.length < 20) {
this.outp.emit(null); this.outp.emit(null);
} else { } else {
fail(`Unexpected MyChild::${LifetimeMethods[method]} recursion`); fail(`Unexpected MyChild::${LifetimeMethods[method]} recursion`);
}
}
if ((options.childThrows & method) !== 0) {
if ((this.thrown & method) === 0) {
this.thrown |= method;
log(`<THROW from MyChild::${LifetimeMethods[method]}>()`);
throw new Error(`Throw from MyChild::${LifetimeMethods[method]}`);
}
}
} }
} }
if ((options.childThrows & method) !== 0) {
@Component({ if ((this.thrown & method) === 0) {
selector: 'my-component', this.thrown |= method;
template: `<my-child [inp]='true' (outp)='onOutp()'></my-child>` log(`<THROW from MyChild::${LifetimeMethods[method]}>()`);
}) throw new Error(`Throw from MyChild::${LifetimeMethods[method]}`);
class MyComponent {
constructor(private changeDetectionRef: ChangeDetectorRef) {}
ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); }
ngOnInit() { this.check(LifetimeMethods.ngOnInit); }
ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); }
ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); }
onOutp() {
log('<RECURSION START>');
this.changeDetectionRef.detectChanges();
log('<RECURSION DONE>');
}
private check(method: LifetimeMethods) {
log(`MyComponent::${LifetimeMethods[method]}()`);
} }
} }
}
}
TestBed.configureTestingModule({declarations: [MyChild, MyComponent]}); @Component({
selector: 'my-component',
return createCompFixture(`<my-component></my-component>`); template: `<my-child [inp]='true' (outp)='onOutp()'></my-child>`
})
class MyComponent {
constructor(private changeDetectionRef: ChangeDetectorRef) {}
ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); }
ngOnInit() { this.check(LifetimeMethods.ngOnInit); }
ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); }
ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); }
onOutp() {
log('<RECURSION START>');
this.changeDetectionRef.detectChanges();
log('<RECURSION DONE>');
} }
function ensureOneInit(options: Options) { private check(method: LifetimeMethods) {
const ctx = initialize(options); log(`MyComponent::${LifetimeMethods[method]}()`);
}
}
TestBed.configureTestingModule({declarations: [MyChild, MyComponent]});
return createCompFixture(`<my-component></my-component>`);
}
function ensureOneInit(options: Options) {
const ctx = initialize(options);
const throws = options.childThrows != LifetimeMethods.None; const throws = options.childThrows != LifetimeMethods.None;
if (throws) { if (throws) {
log(`<CYCLE 0 START>`); log(`<CYCLE 0 START>`);
expect(() => { expect(() => {
// Expect child to throw. // Expect child to throw.
ctx.detectChanges();
}).toThrow();
log(`<CYCLE 0 END>`);
log(`<CYCLE 1 START>`);
}
ctx.detectChanges(); ctx.detectChanges();
if (throws) log(`<CYCLE 1 DONE>`); }).toThrow();
expectOnceAndOnlyOnce('MyComponent::ngOnInit()'); log(`<CYCLE 0 END>`);
expectOnceAndOnlyOnce('MyChild::ngOnInit()'); log(`<CYCLE 1 START>`);
expectOnceAndOnlyOnce('MyComponent::ngAfterViewInit()'); }
expectOnceAndOnlyOnce('MyComponent::ngAfterContentInit()'); ctx.detectChanges();
expectOnceAndOnlyOnce('MyChild::ngAfterViewInit()'); if (throws) log(`<CYCLE 1 DONE>`);
expectOnceAndOnlyOnce('MyChild::ngAfterContentInit()'); expectOnceAndOnlyOnce('MyComponent::ngOnInit()');
} expectOnceAndOnlyOnce('MyChild::ngOnInit()');
expectOnceAndOnlyOnce('MyComponent::ngAfterViewInit()');
expectOnceAndOnlyOnce('MyComponent::ngAfterContentInit()');
expectOnceAndOnlyOnce('MyChild::ngAfterViewInit()');
expectOnceAndOnlyOnce('MyChild::ngAfterContentInit()');
}
forEachMethod(LifetimeMethods.InitMethodsAndChanges, method => { forEachMethod(LifetimeMethods.InitMethodsAndChanges, method => {
it(`should ensure that init hooks are called once an only once with recursion in ${LifetimeMethods[method]} `, it(`should ensure that init hooks are called once an only once with recursion in ${LifetimeMethods[method]} `,
() => { () => {
// Ensure all the init methods are called once. // Ensure all the init methods are called once.
ensureOneInit({childRecursion: method, childThrows: LifetimeMethods.None}); ensureOneInit({childRecursion: method, childThrows: LifetimeMethods.None});
}); });
}); });
forEachMethod(LifetimeMethods.All, method => { forEachMethod(LifetimeMethods.All, method => {
it(`should ensure that init hooks are called once an only once with a throw in ${LifetimeMethods[method]} `, it(`should ensure that init hooks are called once an only once with a throw in ${LifetimeMethods[method]} `,
() => { () => {
// Ensure all the init methods are called once. // Ensure all the init methods are called once.
// the first cycle throws but the next cycle should complete the inits. // the first cycle throws but the next cycle should complete the inits.
ensureOneInit({childRecursion: LifetimeMethods.None, childThrows: method}); ensureOneInit({childRecursion: LifetimeMethods.None, childThrows: method});
}); });
}); });
}); });
}); });
}); });
})(); })();

View File

@ -563,61 +563,70 @@ describe('change detection', () => {
}); });
it('should not go infinite loop when recursively called from children\'s ngOnChanges', () => { ['OnInit', 'AfterContentInit', 'AfterViewInit', 'OnChanges'].forEach(hook => {
class ChildComp { it(`should not go infinite loop when recursively called from children's ng${hook}`, () => {
// @Input class ChildComp {
inp = ''; // @Input
inp = '';
count = 0; count = 0;
constructor(public parentComp: ParentComp) {} constructor(public parentComp: ParentComp) {}
ngOnChanges() { ngOnInit() { this.check('OnInit'); }
this.count++; ngAfterContentInit() { this.check('AfterContentInit'); }
if (this.count > 1) throw new Error(`ngOnChanges should be called only once!`); ngAfterViewInit() { this.check('AfterViewInit'); }
this.parentComp.triggerChangeDetection(); ngOnChanges() { this.check('OnChanges'); }
check(h: string) {
if (h === hook) {
this.count++;
if (this.count > 1) throw new Error(`ng${hook} 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]
});
} }
static ngComponentDef = defineComponent({ class ParentComp {
type: ChildComp, constructor(public cdr: ChangeDetectorRef) {}
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 { triggerChangeDetection() { this.cdr.detectChanges(); }
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]
});
}
static ngComponentDef = defineComponent({ expect(() => renderComponent(ParentComp)).not.toThrow();
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', () => {