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
|
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,
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]) !;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in New Issue