fix(ivy): init hooks should be called once and only once (#28239)
PR Close #28239
This commit is contained in:
parent
873750609f
commit
d83307adab
|
@ -256,6 +256,7 @@ export function defineComponent<T>(componentDefinition: {
|
|||
inputs: null !, // assigned in noSideEffects
|
||||
outputs: null !, // assigned in noSideEffects
|
||||
exportAs: componentDefinition.exportAs || null,
|
||||
onChanges: null,
|
||||
onInit: typePrototype.ngOnInit || null,
|
||||
doCheck: typePrototype.ngDoCheck || null,
|
||||
afterContentInit: typePrototype.ngAfterContentInit || null,
|
||||
|
|
|
@ -49,16 +49,11 @@ export function NgOnChangesFeature<T>(): DirectiveDefFeature {
|
|||
function NgOnChangesFeatureImpl<T>(definition: DirectiveDef<T>): void {
|
||||
if (definition.type.prototype.ngOnChanges) {
|
||||
definition.setInput = ngOnChangesSetInput;
|
||||
|
||||
const prevDoCheck = definition.doCheck;
|
||||
const prevOnInit = definition.onInit;
|
||||
|
||||
definition.onInit = wrapOnChanges(prevOnInit);
|
||||
definition.doCheck = wrapOnChanges(prevDoCheck);
|
||||
definition.onChanges = wrapOnChanges();
|
||||
}
|
||||
}
|
||||
|
||||
function wrapOnChanges(hook: (() => void) | null) {
|
||||
function wrapOnChanges() {
|
||||
return function(this: OnChanges) {
|
||||
const simpleChangesStore = getSimpleChangesStore(this);
|
||||
const current = simpleChangesStore && simpleChangesStore.current;
|
||||
|
@ -68,8 +63,6 @@ function wrapOnChanges(hook: (() => void) | null) {
|
|||
simpleChangesStore !.current = null;
|
||||
this.ngOnChanges(current);
|
||||
}
|
||||
|
||||
hook && hook.call(this);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {SimpleChanges} from '../interface/simple_change';
|
||||
import {assertEqual} from '../util/assert';
|
||||
|
||||
import {DirectiveDef} from './interfaces/definition';
|
||||
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 &&
|
||||
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) {
|
||||
(tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onInit);
|
||||
(tView.initHooks || (tView.initHooks = [])).push(-directiveIndex, onInit);
|
||||
}
|
||||
|
||||
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++) {
|
||||
const directiveDef = tView.data[i] as DirectiveDef<any>;
|
||||
if (directiveDef.afterContentInit) {
|
||||
(tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentInit);
|
||||
(tView.contentHooks || (tView.contentHooks = [])).push(-i, directiveDef.afterContentInit);
|
||||
}
|
||||
|
||||
if (directiveDef.afterContentChecked) {
|
||||
|
@ -83,7 +87,7 @@ export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
|
|||
}
|
||||
|
||||
if (directiveDef.afterViewInit) {
|
||||
(tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewInit);
|
||||
(tView.viewHooks || (tView.viewHooks = [])).push(-i, directiveDef.afterViewInit);
|
||||
}
|
||||
|
||||
if (directiveDef.afterViewChecked) {
|
||||
|
@ -114,9 +118,10 @@ export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
|
|||
*/
|
||||
export function executeInitHooks(
|
||||
currentView: LView, tView: TView, checkNoChangesMode: boolean): void {
|
||||
if (!checkNoChangesMode && currentView[FLAGS] & LViewFlags.RunInit) {
|
||||
executeHooks(currentView, tView.initHooks, tView.checkHooks, checkNoChangesMode);
|
||||
currentView[FLAGS] &= ~LViewFlags.RunInit;
|
||||
if (!checkNoChangesMode) {
|
||||
executeHooks(
|
||||
currentView, tView.initHooks, tView.checkHooks, checkNoChangesMode,
|
||||
InitPhaseState.OnInitHooksToBeRun);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,12 +136,19 @@ export function executeInitHooks(
|
|||
*/
|
||||
export function executeHooks(
|
||||
currentView: LView, firstPassHooks: HookData | null, checkHooks: HookData | null,
|
||||
checkNoChangesMode: boolean): void {
|
||||
checkNoChangesMode: boolean, initPhase: number): void {
|
||||
if (checkNoChangesMode) return;
|
||||
|
||||
const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? firstPassHooks : checkHooks;
|
||||
const hooksToCall = (currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase ?
|
||||
firstPassHooks :
|
||||
checkHooks;
|
||||
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 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) {
|
||||
(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'
|
|||
import {LQueries} from './interfaces/query';
|
||||
import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
|
||||
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 {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
|
||||
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.
|
||||
refreshContentQueries(tView);
|
||||
|
||||
executeHooks(lView, tView.contentHooks, tView.contentCheckHooks, checkNoChangesMode);
|
||||
executeHooks(
|
||||
lView, tView.contentHooks, tView.contentCheckHooks, checkNoChangesMode,
|
||||
InitPhaseState.AfterContentInitHooksToBeRun);
|
||||
|
||||
setHostBindings(tView, lView);
|
||||
}
|
||||
|
@ -159,8 +161,7 @@ export function createLView<T>(
|
|||
rendererFactory?: RendererFactory3 | null, renderer?: Renderer3 | null,
|
||||
sanitizer?: Sanitizer | null, injector?: Injector | null): LView {
|
||||
const lView = tView.blueprint.slice() as LView;
|
||||
lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit |
|
||||
LViewFlags.FirstLViewPass;
|
||||
lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.FirstLViewPass;
|
||||
lView[PARENT] = lView[DECLARATION_VIEW] = parentLView;
|
||||
lView[CONTEXT] = context;
|
||||
lView[RENDERER_FACTORY] = (rendererFactory || parentLView && parentLView[RENDERER_FACTORY]) !;
|
||||
|
|
|
@ -140,6 +140,7 @@ export interface DirectiveDef<T> extends BaseDef<T> {
|
|||
hostBindings: HostBindingsFunction<T>|null;
|
||||
|
||||
/* The following are lifecycle hooks for this component */
|
||||
onChanges: (() => void)|null;
|
||||
onInit: (() => void)|null;
|
||||
doCheck: (() => void)|null;
|
||||
afterContentInit: (() => void)|null;
|
||||
|
|
|
@ -215,6 +215,10 @@ export interface LView extends Array<any> {
|
|||
|
||||
/** Flags associated with an LView (saved in LView[FLAGS]) */
|
||||
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.
|
||||
*
|
||||
|
@ -223,7 +227,7 @@ export const enum LViewFlags {
|
|||
* back into the parent view, `data` will be defined and `creationMode` will be
|
||||
* improperly reported as false.
|
||||
*/
|
||||
CreationMode = 0b000000001,
|
||||
CreationMode = 0b00000000100,
|
||||
|
||||
/**
|
||||
* 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
|
||||
* time, the flag is turned off.
|
||||
*/
|
||||
FirstLViewPass = 0b000000010,
|
||||
FirstLViewPass = 0b00000001000,
|
||||
|
||||
/** 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) */
|
||||
Dirty = 0b000001000,
|
||||
Dirty = 0b00000100000,
|
||||
|
||||
/** Whether or not this view is currently attached to change detection tree. */
|
||||
Attached = 0b000010000,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
Attached = 0b00001000000,
|
||||
|
||||
/** Whether or not this view is destroyed. */
|
||||
Destroyed = 0b001000000,
|
||||
Destroyed = 0b00010000000,
|
||||
|
||||
/** 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,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,7 +11,7 @@ import {executeHooks} from './hooks';
|
|||
import {ComponentDef, DirectiveDef} from './interfaces/definition';
|
||||
import {TElementNode, TNode, TNodeFlags, TViewNode} from './interfaces/node';
|
||||
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';
|
||||
|
||||
|
||||
|
@ -336,11 +336,12 @@ export function leaveView(newView: LView): void {
|
|||
lView[FLAGS] &= ~LViewFlags.CreationMode;
|
||||
} else {
|
||||
try {
|
||||
executeHooks(lView, tView.viewHooks, tView.viewCheckHooks, checkNoChangesMode);
|
||||
executeHooks(
|
||||
lView, tView.viewHooks, tView.viewCheckHooks, checkNoChangesMode,
|
||||
InitPhaseState.AfterViewInitHooksToBeRun);
|
||||
} finally {
|
||||
// Views are clean and in update mode after being checked, so these bits are cleared
|
||||
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
|
||||
lView[FLAGS] |= LViewFlags.RunInit;
|
||||
lView[BINDING_INDEX] = tView.bindingStartIndex;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1588,111 +1588,110 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
|
|||
childThrows: LifetimeMethods;
|
||||
}
|
||||
|
||||
fixmeIvy('FW-832: View engine supports recursive detectChanges() calls')
|
||||
.describe('calling init', () => {
|
||||
function initialize(options: Options) {
|
||||
@Component({selector: 'my-child', template: ''})
|
||||
class MyChild {
|
||||
private thrown = LifetimeMethods.None;
|
||||
describe('calling init', () => {
|
||||
function initialize(options: Options) {
|
||||
@Component({selector: 'my-child', template: ''})
|
||||
class MyChild {
|
||||
private thrown = LifetimeMethods.None;
|
||||
|
||||
// TODO(issue/24571): remove '!'.
|
||||
@Input() inp !: boolean;
|
||||
@Output() outp = new EventEmitter<any>();
|
||||
// TODO(issue/24571): remove '!'.
|
||||
@Input() inp !: boolean;
|
||||
@Output() outp = new EventEmitter<any>();
|
||||
|
||||
constructor() {}
|
||||
constructor() {}
|
||||
|
||||
ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); }
|
||||
ngOnInit() { this.check(LifetimeMethods.ngOnInit); }
|
||||
ngOnChanges() { this.check(LifetimeMethods.ngOnChanges); }
|
||||
ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); }
|
||||
ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); }
|
||||
ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); }
|
||||
ngOnInit() { this.check(LifetimeMethods.ngOnInit); }
|
||||
ngOnChanges() { this.check(LifetimeMethods.ngOnChanges); }
|
||||
ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); }
|
||||
ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); }
|
||||
|
||||
private check(method: LifetimeMethods) {
|
||||
log(`MyChild::${LifetimeMethods[method]}()`);
|
||||
private check(method: LifetimeMethods) {
|
||||
log(`MyChild::${LifetimeMethods[method]}()`);
|
||||
|
||||
if ((options.childRecursion & method) !== 0) {
|
||||
if (logged.length < 20) {
|
||||
this.outp.emit(null);
|
||||
} else {
|
||||
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.childRecursion & method) !== 0) {
|
||||
if (logged.length < 20) {
|
||||
this.outp.emit(null);
|
||||
} else {
|
||||
fail(`Unexpected MyChild::${LifetimeMethods[method]} recursion`);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: '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>');
|
||||
}
|
||||
|
||||
private check(method: LifetimeMethods) {
|
||||
log(`MyComponent::${LifetimeMethods[method]}()`);
|
||||
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]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyChild, MyComponent]});
|
||||
|
||||
return createCompFixture(`<my-component></my-component>`);
|
||||
@Component({
|
||||
selector: '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) {
|
||||
const ctx = initialize(options);
|
||||
private check(method: LifetimeMethods) {
|
||||
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;
|
||||
if (throws) {
|
||||
log(`<CYCLE 0 START>`);
|
||||
expect(() => {
|
||||
// Expect child to throw.
|
||||
ctx.detectChanges();
|
||||
}).toThrow();
|
||||
log(`<CYCLE 0 END>`);
|
||||
log(`<CYCLE 1 START>`);
|
||||
}
|
||||
const throws = options.childThrows != LifetimeMethods.None;
|
||||
if (throws) {
|
||||
log(`<CYCLE 0 START>`);
|
||||
expect(() => {
|
||||
// Expect child to throw.
|
||||
ctx.detectChanges();
|
||||
if (throws) log(`<CYCLE 1 DONE>`);
|
||||
expectOnceAndOnlyOnce('MyComponent::ngOnInit()');
|
||||
expectOnceAndOnlyOnce('MyChild::ngOnInit()');
|
||||
expectOnceAndOnlyOnce('MyComponent::ngAfterViewInit()');
|
||||
expectOnceAndOnlyOnce('MyComponent::ngAfterContentInit()');
|
||||
expectOnceAndOnlyOnce('MyChild::ngAfterViewInit()');
|
||||
expectOnceAndOnlyOnce('MyChild::ngAfterContentInit()');
|
||||
}
|
||||
}).toThrow();
|
||||
log(`<CYCLE 0 END>`);
|
||||
log(`<CYCLE 1 START>`);
|
||||
}
|
||||
ctx.detectChanges();
|
||||
if (throws) log(`<CYCLE 1 DONE>`);
|
||||
expectOnceAndOnlyOnce('MyComponent::ngOnInit()');
|
||||
expectOnceAndOnlyOnce('MyChild::ngOnInit()');
|
||||
expectOnceAndOnlyOnce('MyComponent::ngAfterViewInit()');
|
||||
expectOnceAndOnlyOnce('MyComponent::ngAfterContentInit()');
|
||||
expectOnceAndOnlyOnce('MyChild::ngAfterViewInit()');
|
||||
expectOnceAndOnlyOnce('MyChild::ngAfterContentInit()');
|
||||
}
|
||||
|
||||
forEachMethod(LifetimeMethods.InitMethodsAndChanges, 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.
|
||||
ensureOneInit({childRecursion: method, childThrows: LifetimeMethods.None});
|
||||
});
|
||||
});
|
||||
forEachMethod(LifetimeMethods.All, 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.
|
||||
// the first cycle throws but the next cycle should complete the inits.
|
||||
ensureOneInit({childRecursion: LifetimeMethods.None, childThrows: method});
|
||||
});
|
||||
});
|
||||
});
|
||||
forEachMethod(LifetimeMethods.InitMethodsAndChanges, 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.
|
||||
ensureOneInit({childRecursion: method, childThrows: LifetimeMethods.None});
|
||||
});
|
||||
});
|
||||
forEachMethod(LifetimeMethods.All, 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.
|
||||
// the first cycle throws but the next cycle should complete the inits.
|
||||
ensureOneInit({childRecursion: LifetimeMethods.None, childThrows: method});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -563,61 +563,70 @@ describe('change detection', () => {
|
|||
});
|
||||
|
||||
|
||||
it('should not go infinite loop when recursively called from children\'s ngOnChanges', () => {
|
||||
class ChildComp {
|
||||
// @Input
|
||||
inp = '';
|
||||
['OnInit', 'AfterContentInit', 'AfterViewInit', 'OnChanges'].forEach(hook => {
|
||||
it(`should not go infinite loop when recursively called from children's ng${hook}`, () => {
|
||||
class ChildComp {
|
||||
// @Input
|
||||
inp = '';
|
||||
|
||||
count = 0;
|
||||
constructor(public parentComp: ParentComp) {}
|
||||
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();
|
||||
ngOnInit() { this.check('OnInit'); }
|
||||
ngAfterContentInit() { this.check('AfterContentInit'); }
|
||||
ngAfterViewInit() { this.check('AfterViewInit'); }
|
||||
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({
|
||||
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) {}
|
||||
|
||||
class ParentComp {
|
||||
constructor(public cdr: ChangeDetectorRef) {}
|
||||
triggerChangeDetection() { this.cdr.detectChanges(); }
|
||||
|
||||
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({
|
||||
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();
|
||||
expect(() => renderComponent(ParentComp)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support call in ngDoCheck', () => {
|
||||
|
|
Loading…
Reference in New Issue