fix(core): Allow modification of lifecycle hooks any time before bootstrap (#35464)
Currently we read lifecycle hooks eagerly during `ɵɵdefineComponent`. The result is that it is not possible to do any sort of meta-programing such as mixins or adding lifecycle hooks using custom decorators since any such code executes after `ɵɵdefineComponent` has extracted the lifecycle hooks from the prototype. Additionally the behavior is inconsistent between AOT and JIT mode. In JIT mode overriding lifecycle hooks is possible because the whole `ɵɵdefineComponent` is placed in getter which is executed lazily. This is because JIT mode must compile a template which can be specified as `templateURL` and those we are waiting for its resolution. - `+` `ɵɵdefineComponent` becomes smaller as it no longer needs to copy lifecycle hooks from prototype to `ComponentDef` - `-` `ɵɵNgOnChangesFeature` feature is now always included with the codebase as it is no longer tree shakable. Previously we have read lifecycle hooks from prototype in the `ɵɵdefineComponent` so that lifecycle hook access would be monomorphic. This decision was made before we had `T*` data structures. By not reading the lifecycle hooks we are moving the megamorhic read form `ɵɵdefineComponent` to instructions. However, the reads happen on `firstTemplatePass` only and are subsequently cached in the `T*` data structures. The result is that the overall performance should be same (or slightly better as the intermediate `ComponentDef` has been removed.) - [ ] Remove `ɵɵNgOnChangesFeature` from compiler. (It will no longer be a feature.) - [ ] Discuss the future of `Features` as they hinder meta-programing. Fix #30497 PR Close #35464
This commit is contained in:
parent
469aba0140
commit
737506e79c
|
@ -894,7 +894,7 @@ export declare function ɵɵnextContext<T = any>(level?: number): T;
|
||||||
|
|
||||||
export declare type ɵɵNgModuleDefWithMeta<T, Declarations, Imports, Exports> = ɵNgModuleDef<T>;
|
export declare type ɵɵNgModuleDefWithMeta<T, Declarations, Imports, Exports> = ɵNgModuleDef<T>;
|
||||||
|
|
||||||
export declare function ɵɵNgOnChangesFeature<T>(definition: ɵDirectiveDef<T>): void;
|
export declare function ɵɵNgOnChangesFeature<T>(): DirectiveDefFeature;
|
||||||
|
|
||||||
export declare function ɵɵpipe(index: number, pipeName: string): any;
|
export declare function ɵɵpipe(index: number, pipeName: string): any;
|
||||||
|
|
||||||
|
@ -1056,7 +1056,7 @@ export declare function ɵɵstylePropInterpolateV(prop: string, values: any[], v
|
||||||
|
|
||||||
export declare function ɵɵtemplate(index: number, templateFn: ComponentTemplate<any> | null, decls: number, vars: number, tagName?: string | null, attrsIndex?: number | null, localRefsIndex?: number | null, localRefExtractor?: LocalRefExtractor): void;
|
export declare function ɵɵtemplate(index: number, templateFn: ComponentTemplate<any> | null, decls: number, vars: number, tagName?: string | null, attrsIndex?: number | null, localRefsIndex?: number | null, localRefExtractor?: LocalRefExtractor): void;
|
||||||
|
|
||||||
export declare function ɵɵtemplateRefExtractor(tNode: TNode, currentView: ɵangular_packages_core_core_bo): TemplateRef<unknown> | null;
|
export declare function ɵɵtemplateRefExtractor(tNode: TNode, currentView: ɵangular_packages_core_core_bp): TemplateRef<unknown> | null;
|
||||||
|
|
||||||
export declare function ɵɵtext(index: number, value?: string): void;
|
export declare function ɵɵtext(index: number, value?: string): void;
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 2987,
|
"runtime-es2015": 2987,
|
||||||
"main-es2015": 450883,
|
"main-es2015": 450301,
|
||||||
"polyfills-es2015": 52630
|
"polyfills-es2015": 52630
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,4 +26,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -12,8 +12,8 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 1485,
|
"runtime-es2015": 1485,
|
||||||
"main-es2015": 16959,
|
"main-es2015": 17362,
|
||||||
"polyfills-es2015": 36938
|
"polyfills-es2015": 36657
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 1485,
|
"runtime-es2015": 1485,
|
||||||
"main-es2015": 147314,
|
"main-es2015": 147573,
|
||||||
"polyfills-es2015": 36571
|
"polyfills-es2015": 36571
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 1485,
|
"runtime-es2015": 1485,
|
||||||
"main-es2015": 135533,
|
"main-es2015": 136168,
|
||||||
"polyfills-es2015": 37248
|
"polyfills-es2015": 37248
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 2289,
|
"runtime-es2015": 2289,
|
||||||
"main-es2015": 246044,
|
"main-es2015": 245351,
|
||||||
"polyfills-es2015": 36938,
|
"polyfills-es2015": 36938,
|
||||||
"5-es2015": 751
|
"5-es2015": 751
|
||||||
}
|
}
|
||||||
|
@ -66,4 +66,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -290,72 +290,65 @@ export function ɵɵdefineComponent<T>(componentDefinition: {
|
||||||
schemas?: SchemaMetadata[] | null;
|
schemas?: SchemaMetadata[] | null;
|
||||||
}): never {
|
}): never {
|
||||||
return noSideEffects(() => {
|
return noSideEffects(() => {
|
||||||
// Initialize ngDevMode. This must be the first statement in ɵɵdefineComponent.
|
// Initialize ngDevMode. This must be the first statement in ɵɵdefineComponent.
|
||||||
// See the `initNgDevMode` docstring for more information.
|
// See the `initNgDevMode` docstring for more information.
|
||||||
(typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode();
|
(typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode();
|
||||||
|
|
||||||
const type = componentDefinition.type;
|
const type = componentDefinition.type;
|
||||||
const typePrototype = type.prototype;
|
const typePrototype = type.prototype;
|
||||||
const declaredInputs: {[key: string]: string} = {} as any;
|
const declaredInputs: {[key: string]: string} = {} as any;
|
||||||
const def: Mutable<ComponentDef<any>, keyof ComponentDef<any>> = {
|
const def: Mutable<ComponentDef<any>, keyof ComponentDef<any>> = {
|
||||||
type: type,
|
type: type,
|
||||||
providersResolver: null,
|
providersResolver: null,
|
||||||
decls: componentDefinition.decls,
|
decls: componentDefinition.decls,
|
||||||
vars: componentDefinition.vars,
|
vars: componentDefinition.vars,
|
||||||
factory: null,
|
factory: null,
|
||||||
template: componentDefinition.template || null!,
|
template: componentDefinition.template || null!,
|
||||||
consts: componentDefinition.consts || null,
|
consts: componentDefinition.consts || null,
|
||||||
ngContentSelectors: componentDefinition.ngContentSelectors,
|
ngContentSelectors: componentDefinition.ngContentSelectors,
|
||||||
hostBindings: componentDefinition.hostBindings || null,
|
hostBindings: componentDefinition.hostBindings || null,
|
||||||
hostVars: componentDefinition.hostVars || 0,
|
hostVars: componentDefinition.hostVars || 0,
|
||||||
hostAttrs: componentDefinition.hostAttrs || null,
|
hostAttrs: componentDefinition.hostAttrs || null,
|
||||||
contentQueries: componentDefinition.contentQueries || null,
|
contentQueries: componentDefinition.contentQueries || null,
|
||||||
declaredInputs: declaredInputs,
|
declaredInputs: declaredInputs,
|
||||||
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,
|
onPush: componentDefinition.changeDetection === ChangeDetectionStrategy.OnPush,
|
||||||
onInit: typePrototype.ngOnInit || null,
|
directiveDefs: null!, // assigned in noSideEffects
|
||||||
doCheck: typePrototype.ngDoCheck || null,
|
pipeDefs: null!, // assigned in noSideEffects
|
||||||
afterContentInit: typePrototype.ngAfterContentInit || null,
|
selectors: componentDefinition.selectors || EMPTY_ARRAY,
|
||||||
afterContentChecked: typePrototype.ngAfterContentChecked || null,
|
viewQuery: componentDefinition.viewQuery || null,
|
||||||
afterViewInit: typePrototype.ngAfterViewInit || null,
|
features: componentDefinition.features as DirectiveDefFeature[] || null,
|
||||||
afterViewChecked: typePrototype.ngAfterViewChecked || null,
|
data: componentDefinition.data || {},
|
||||||
onDestroy: typePrototype.ngOnDestroy || null,
|
// TODO(misko): convert ViewEncapsulation into const enum so that it can be used
|
||||||
onPush: componentDefinition.changeDetection === ChangeDetectionStrategy.OnPush,
|
// directly in the next line. Also `None` should be 0 not 2.
|
||||||
directiveDefs: null!, // assigned in noSideEffects
|
encapsulation: componentDefinition.encapsulation || ViewEncapsulation.Emulated,
|
||||||
pipeDefs: null!, // assigned in noSideEffects
|
id: 'c',
|
||||||
selectors: componentDefinition.selectors || EMPTY_ARRAY,
|
styles: componentDefinition.styles || EMPTY_ARRAY,
|
||||||
viewQuery: componentDefinition.viewQuery || null,
|
_: null as never,
|
||||||
features: componentDefinition.features as DirectiveDefFeature[] || null,
|
setInput: null,
|
||||||
data: componentDefinition.data || {},
|
schemas: componentDefinition.schemas || null,
|
||||||
// TODO(misko): convert ViewEncapsulation into const enum so that it can be used directly in
|
tView: null,
|
||||||
// the next line. Also `None` should be 0 not 2.
|
};
|
||||||
encapsulation: componentDefinition.encapsulation || ViewEncapsulation.Emulated,
|
const directiveTypes = componentDefinition.directives!;
|
||||||
id: 'c',
|
const feature = componentDefinition.features;
|
||||||
styles: componentDefinition.styles || EMPTY_ARRAY,
|
const pipeTypes = componentDefinition.pipes!;
|
||||||
_: null as never,
|
def.id += _renderCompCount++;
|
||||||
setInput: null,
|
def.inputs = invertObject(componentDefinition.inputs, declaredInputs),
|
||||||
schemas: componentDefinition.schemas || null,
|
def.outputs = invertObject(componentDefinition.outputs),
|
||||||
tView: null,
|
feature && feature.forEach((fn) => fn(def));
|
||||||
};
|
def.directiveDefs = directiveTypes ?
|
||||||
const directiveTypes = componentDefinition.directives!;
|
() => (typeof directiveTypes === 'function' ? directiveTypes() : directiveTypes)
|
||||||
const feature = componentDefinition.features;
|
.map(extractDirectiveDef) :
|
||||||
const pipeTypes = componentDefinition.pipes!;
|
null;
|
||||||
def.id += _renderCompCount++;
|
def.pipeDefs = pipeTypes ?
|
||||||
def.inputs = invertObject(componentDefinition.inputs, declaredInputs),
|
() =>
|
||||||
def.outputs = invertObject(componentDefinition.outputs),
|
(typeof pipeTypes === 'function' ? pipeTypes() : pipeTypes).map(extractPipeDef) :
|
||||||
feature && feature.forEach((fn) => fn(def));
|
null;
|
||||||
def.directiveDefs = directiveTypes ?
|
|
||||||
() => (typeof directiveTypes === 'function' ? directiveTypes() : directiveTypes)
|
|
||||||
.map(extractDirectiveDef) :
|
|
||||||
null;
|
|
||||||
def.pipeDefs = pipeTypes ?
|
|
||||||
() => (typeof pipeTypes === 'function' ? pipeTypes() : pipeTypes).map(extractPipeDef) :
|
|
||||||
null;
|
|
||||||
|
|
||||||
return def as never;
|
return def as never;
|
||||||
});
|
}) as never;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -78,17 +78,6 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef<any>|Compo
|
||||||
const defData = (definition as ComponentDef<any>).data;
|
const defData = (definition as ComponentDef<any>).data;
|
||||||
defData.animation = (defData.animation || []).concat(superDef.data.animation);
|
defData.animation = (defData.animation || []).concat(superDef.data.animation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inherit hooks
|
|
||||||
// Assume super class inheritance feature has already run.
|
|
||||||
writeableDef.afterContentChecked =
|
|
||||||
writeableDef.afterContentChecked || superDef.afterContentChecked;
|
|
||||||
writeableDef.afterContentInit = definition.afterContentInit || superDef.afterContentInit;
|
|
||||||
writeableDef.afterViewChecked = definition.afterViewChecked || superDef.afterViewChecked;
|
|
||||||
writeableDef.afterViewInit = definition.afterViewInit || superDef.afterViewInit;
|
|
||||||
writeableDef.doCheck = definition.doCheck || superDef.doCheck;
|
|
||||||
writeableDef.onDestroy = definition.onDestroy || superDef.onDestroy;
|
|
||||||
writeableDef.onInit = definition.onInit || superDef.onInit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run parent features
|
// Run parent features
|
||||||
|
|
|
@ -11,14 +11,6 @@ import {SimpleChange, SimpleChanges} from '../../interface/simple_change';
|
||||||
import {EMPTY_OBJ} from '../empty';
|
import {EMPTY_OBJ} from '../empty';
|
||||||
import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition';
|
import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition';
|
||||||
|
|
||||||
const PRIVATE_PREFIX = '__ngOnChanges_';
|
|
||||||
|
|
||||||
type OnChangesExpando = OnChanges&{
|
|
||||||
__ngOnChanges_: SimpleChanges|null|undefined;
|
|
||||||
// tslint:disable-next-line:no-any Can hold any value
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The NgOnChangesFeature decorates a component with support for the ngOnChanges
|
* The NgOnChangesFeature decorates a component with support for the ngOnChanges
|
||||||
* lifecycle hook, so it should be included in any component that implements
|
* lifecycle hook, so it should be included in any component that implements
|
||||||
|
@ -41,12 +33,15 @@ type OnChangesExpando = OnChanges&{
|
||||||
*
|
*
|
||||||
* @codeGenApi
|
* @codeGenApi
|
||||||
*/
|
*/
|
||||||
|
export function ɵɵNgOnChangesFeature<T>(): DirectiveDefFeature {
|
||||||
|
return NgOnChangesFeatureImpl;
|
||||||
|
}
|
||||||
|
|
||||||
export function ɵɵNgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
|
export function NgOnChangesFeatureImpl<T>(definition: DirectiveDef<T>) {
|
||||||
if (definition.type.prototype.ngOnChanges) {
|
if (definition.type.prototype.ngOnChanges) {
|
||||||
definition.setInput = ngOnChangesSetInput;
|
definition.setInput = ngOnChangesSetInput;
|
||||||
(definition as {onChanges: Function}).onChanges = wrapOnChanges();
|
|
||||||
}
|
}
|
||||||
|
return rememberChangeHistoryAndInvokeOnChangesHook;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This option ensures that the ngOnChanges lifecycle hook will be inherited
|
// This option ensures that the ngOnChanges lifecycle hook will be inherited
|
||||||
|
@ -55,28 +50,37 @@ export function ɵɵNgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
|
||||||
// tslint:disable-next-line:no-toplevel-property-access
|
// tslint:disable-next-line:no-toplevel-property-access
|
||||||
(ɵɵNgOnChangesFeature as DirectiveDefFeature).ngInherit = true;
|
(ɵɵNgOnChangesFeature as DirectiveDefFeature).ngInherit = true;
|
||||||
|
|
||||||
function wrapOnChanges() {
|
/**
|
||||||
return function wrapOnChangesHook_inPreviousChangesStorage(this: OnChanges) {
|
* This is a synthetic lifecycle hook which gets inserted into `TView.preOrderHooks` to simulate
|
||||||
const simpleChangesStore = getSimpleChangesStore(this);
|
* `ngOnChanges`.
|
||||||
const current = simpleChangesStore && simpleChangesStore.current;
|
*
|
||||||
|
* The hook reads the `NgSimpleChangesStore` data from the component instance and if changes are
|
||||||
|
* found it invokes `ngOnChanges` on the component instance.
|
||||||
|
*
|
||||||
|
* @param this Component instance. Because this function gets inserted into `TView.preOrderHooks`,
|
||||||
|
* it is guaranteed to be called with component instance.
|
||||||
|
*/
|
||||||
|
function rememberChangeHistoryAndInvokeOnChangesHook(this: OnChanges) {
|
||||||
|
const simpleChangesStore = getSimpleChangesStore(this);
|
||||||
|
const current = simpleChangesStore?.current;
|
||||||
|
|
||||||
if (current) {
|
if (current) {
|
||||||
const previous = simpleChangesStore!.previous;
|
const previous = simpleChangesStore!.previous;
|
||||||
if (previous === EMPTY_OBJ) {
|
if (previous === EMPTY_OBJ) {
|
||||||
simpleChangesStore!.previous = current;
|
simpleChangesStore!.previous = current;
|
||||||
} else {
|
} else {
|
||||||
// New changes are copied to the previous store, so that we don't lose history for inputs
|
// New changes are copied to the previous store, so that we don't lose history for inputs
|
||||||
// which were not changed this time
|
// which were not changed this time
|
||||||
for (let key in current) {
|
for (let key in current) {
|
||||||
previous[key] = current[key];
|
previous[key] = current[key];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
simpleChangesStore!.current = null;
|
|
||||||
this.ngOnChanges(current);
|
|
||||||
}
|
}
|
||||||
};
|
simpleChangesStore!.current = null;
|
||||||
|
this.ngOnChanges(current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function ngOnChangesSetInput<T>(
|
function ngOnChangesSetInput<T>(
|
||||||
this: DirectiveDef<T>, instance: T, value: any, publicName: string, privateName: string): void {
|
this: DirectiveDef<T>, instance: T, value: any, publicName: string, privateName: string): void {
|
||||||
const simpleChangesStore = getSimpleChangesStore(instance) ||
|
const simpleChangesStore = getSimpleChangesStore(instance) ||
|
||||||
|
@ -102,6 +106,10 @@ function setSimpleChangesStore(instance: any, store: NgSimpleChangesStore): NgSi
|
||||||
return instance[SIMPLE_CHANGES_STORE] = store;
|
return instance[SIMPLE_CHANGES_STORE] = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data structure which is monkey-patched on the component instance and used by `ngOnChanges`
|
||||||
|
* life-cycle hook to track previous input values.
|
||||||
|
*/
|
||||||
interface NgSimpleChangesStore {
|
interface NgSimpleChangesStore {
|
||||||
previous: SimpleChanges;
|
previous: SimpleChanges;
|
||||||
current: SimpleChanges|null;
|
current: SimpleChanges|null;
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnDestroy, OnInit} from '../interface/lifecycle_hooks';
|
||||||
import {assertEqual, assertNotEqual} from '../util/assert';
|
import {assertEqual, assertNotEqual} from '../util/assert';
|
||||||
|
|
||||||
import {assertFirstCreatePass} from './assert';
|
import {assertFirstCreatePass} from './assert';
|
||||||
|
import {NgOnChangesFeatureImpl} from './features/ng_onchanges_feature';
|
||||||
import {DirectiveDef} from './interfaces/definition';
|
import {DirectiveDef} from './interfaces/definition';
|
||||||
import {TNode} from './interfaces/node';
|
import {TNode} from './interfaces/node';
|
||||||
import {FLAGS, HookData, InitPhaseState, LView, LViewFlags, PREORDER_HOOK_FLAGS, PreOrderHookFlags, TView} from './interfaces/view';
|
import {FLAGS, HookData, InitPhaseState, LView, LViewFlags, PREORDER_HOOK_FLAGS, PreOrderHookFlags, TView} from './interfaces/view';
|
||||||
|
@ -31,20 +32,23 @@ import {getCheckNoChangesMode} from './state';
|
||||||
export function registerPreOrderHooks(
|
export function registerPreOrderHooks(
|
||||||
directiveIndex: number, directiveDef: DirectiveDef<any>, tView: TView): void {
|
directiveIndex: number, directiveDef: DirectiveDef<any>, tView: TView): void {
|
||||||
ngDevMode && assertFirstCreatePass(tView);
|
ngDevMode && assertFirstCreatePass(tView);
|
||||||
const {onChanges, onInit, doCheck} = directiveDef;
|
const {ngOnChanges, ngOnInit, ngDoCheck} =
|
||||||
|
directiveDef.type.prototype as OnChanges & OnInit & DoCheck;
|
||||||
|
|
||||||
if (onChanges) {
|
if (ngOnChanges as Function | undefined) {
|
||||||
(tView.preOrderHooks || (tView.preOrderHooks = [])).push(directiveIndex, onChanges);
|
const wrappedOnChanges = NgOnChangesFeatureImpl(directiveDef);
|
||||||
(tView.preOrderCheckHooks || (tView.preOrderCheckHooks = [])).push(directiveIndex, onChanges);
|
(tView.preOrderHooks || (tView.preOrderHooks = [])).push(directiveIndex, wrappedOnChanges);
|
||||||
|
(tView.preOrderCheckHooks || (tView.preOrderCheckHooks = []))
|
||||||
|
.push(directiveIndex, wrappedOnChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onInit) {
|
if (ngOnInit) {
|
||||||
(tView.preOrderHooks || (tView.preOrderHooks = [])).push(-directiveIndex, onInit);
|
(tView.preOrderHooks || (tView.preOrderHooks = [])).push(0 - directiveIndex, ngOnInit);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doCheck) {
|
if (ngDoCheck) {
|
||||||
(tView.preOrderHooks || (tView.preOrderHooks = [])).push(directiveIndex, doCheck);
|
(tView.preOrderHooks || (tView.preOrderHooks = [])).push(directiveIndex, ngDoCheck);
|
||||||
(tView.preOrderCheckHooks || (tView.preOrderCheckHooks = [])).push(directiveIndex, doCheck);
|
(tView.preOrderCheckHooks || (tView.preOrderCheckHooks = [])).push(directiveIndex, ngDoCheck);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,27 +77,36 @@ export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
|
||||||
// hooks for projected components and directives must be called *before* their hosts.
|
// hooks for projected components and directives must be called *before* their hosts.
|
||||||
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) {
|
const lifecycleHooks: AfterContentInit&AfterContentChecked&AfterViewInit&AfterViewChecked&
|
||||||
(tView.contentHooks || (tView.contentHooks = [])).push(-i, directiveDef.afterContentInit);
|
OnDestroy = directiveDef.type.prototype;
|
||||||
|
const {
|
||||||
|
ngAfterContentInit,
|
||||||
|
ngAfterContentChecked,
|
||||||
|
ngAfterViewInit,
|
||||||
|
ngAfterViewChecked,
|
||||||
|
ngOnDestroy
|
||||||
|
} = lifecycleHooks;
|
||||||
|
|
||||||
|
if (ngAfterContentInit) {
|
||||||
|
(tView.contentHooks || (tView.contentHooks = [])).push(-i, ngAfterContentInit);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directiveDef.afterContentChecked) {
|
if (ngAfterContentChecked) {
|
||||||
(tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentChecked);
|
(tView.contentHooks || (tView.contentHooks = [])).push(i, ngAfterContentChecked);
|
||||||
(tView.contentCheckHooks || (tView.contentCheckHooks = []))
|
(tView.contentCheckHooks || (tView.contentCheckHooks = [])).push(i, ngAfterContentChecked);
|
||||||
.push(i, directiveDef.afterContentChecked);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directiveDef.afterViewInit) {
|
if (ngAfterViewInit) {
|
||||||
(tView.viewHooks || (tView.viewHooks = [])).push(-i, directiveDef.afterViewInit);
|
(tView.viewHooks || (tView.viewHooks = [])).push(-i, ngAfterViewInit);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directiveDef.afterViewChecked) {
|
if (ngAfterViewChecked) {
|
||||||
(tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewChecked);
|
(tView.viewHooks || (tView.viewHooks = [])).push(i, ngAfterViewChecked);
|
||||||
(tView.viewCheckHooks || (tView.viewCheckHooks = [])).push(i, directiveDef.afterViewChecked);
|
(tView.viewCheckHooks || (tView.viewCheckHooks = [])).push(i, ngAfterViewChecked);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directiveDef.onDestroy != null) {
|
if (ngOnDestroy != null) {
|
||||||
(tView.destroyHooks || (tView.destroyHooks = [])).push(i, directiveDef.onDestroy);
|
(tView.destroyHooks || (tView.destroyHooks = [])).push(i, ngOnDestroy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
import {Injector} from '../../di';
|
import {Injector} from '../../di';
|
||||||
import {ErrorHandler} from '../../error_handler';
|
import {ErrorHandler} from '../../error_handler';
|
||||||
|
import {DoCheck, OnChanges, OnInit} from '../../interface/lifecycle_hooks';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from '../../metadata/schema';
|
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from '../../metadata/schema';
|
||||||
import {ViewEncapsulation} from '../../metadata/view';
|
import {ViewEncapsulation} from '../../metadata/view';
|
||||||
import {validateAgainstEventAttributes, validateAgainstEventProperties} from '../../sanitization/sanitization';
|
import {validateAgainstEventAttributes, validateAgainstEventProperties} from '../../sanitization/sanitization';
|
||||||
|
@ -1166,9 +1167,11 @@ export function resolveDirectives(
|
||||||
if (def.hostBindings !== null || def.hostAttrs !== null || def.hostVars !== 0)
|
if (def.hostBindings !== null || def.hostAttrs !== null || def.hostVars !== 0)
|
||||||
tNode.flags |= TNodeFlags.hasHostBindings;
|
tNode.flags |= TNodeFlags.hasHostBindings;
|
||||||
|
|
||||||
|
const lifeCycleHooks: OnChanges&OnInit&DoCheck = def.type.prototype;
|
||||||
// Only push a node index into the preOrderHooks array if this is the first
|
// Only push a node index into the preOrderHooks array if this is the first
|
||||||
// pre-order hook found on this node.
|
// pre-order hook found on this node.
|
||||||
if (!preOrderHooksFound && (def.onChanges || def.onInit || def.doCheck)) {
|
if (!preOrderHooksFound &&
|
||||||
|
(lifeCycleHooks.ngOnChanges || lifeCycleHooks.ngOnInit || lifeCycleHooks.ngDoCheck)) {
|
||||||
// We will push the actual hook function into this array later during dir instantiation.
|
// We will push the actual hook function into this array later during dir instantiation.
|
||||||
// We cannot do it now because we must ensure hooks are registered in the same
|
// We cannot do it now because we must ensure hooks are registered in the same
|
||||||
// order that directives are created (i.e. injection order).
|
// order that directives are created (i.e. injection order).
|
||||||
|
@ -1176,7 +1179,7 @@ export function resolveDirectives(
|
||||||
preOrderHooksFound = true;
|
preOrderHooksFound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preOrderCheckHooksFound && (def.onChanges || def.doCheck)) {
|
if (!preOrderCheckHooksFound && (lifeCycleHooks.ngOnChanges || lifeCycleHooks.ngDoCheck)) {
|
||||||
(tView.preOrderCheckHooks || (tView.preOrderCheckHooks = []))
|
(tView.preOrderCheckHooks || (tView.preOrderCheckHooks = []))
|
||||||
.push(tNode.index - HEADER_OFFSET);
|
.push(tNode.index - HEADER_OFFSET);
|
||||||
preOrderCheckHooksFound = true;
|
preOrderCheckHooksFound = true;
|
||||||
|
|
|
@ -250,16 +250,6 @@ export interface DirectiveDef<T> {
|
||||||
*/
|
*/
|
||||||
readonly factory: FactoryFn<T>|null;
|
readonly factory: FactoryFn<T>|null;
|
||||||
|
|
||||||
/* The following are lifecycle hooks for this component */
|
|
||||||
readonly onChanges: (() => void)|null;
|
|
||||||
readonly onInit: (() => void)|null;
|
|
||||||
readonly doCheck: (() => void)|null;
|
|
||||||
readonly afterContentInit: (() => void)|null;
|
|
||||||
readonly afterContentChecked: (() => void)|null;
|
|
||||||
readonly afterViewInit: (() => void)|null;
|
|
||||||
readonly afterViewChecked: (() => void)|null;
|
|
||||||
readonly onDestroy: (() => void)|null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The features applied to this directive
|
* The features applied to this directive
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1124,6 +1124,100 @@ describe('onChanges', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('meta-programing', () => {
|
||||||
|
it('should allow adding lifecycle hook methods any time before first instance creation', () => {
|
||||||
|
const events: any[] = [];
|
||||||
|
|
||||||
|
@Component({template: `<child name="value"></child>`})
|
||||||
|
class App {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'child', template: `empty`})
|
||||||
|
class Child {
|
||||||
|
@Input() name: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChildPrototype = Child.prototype as any;
|
||||||
|
ChildPrototype.ngOnInit = () => events.push('onInit');
|
||||||
|
ChildPrototype.ngOnChanges = (e: SimpleChanges) => {
|
||||||
|
const name = e['name'];
|
||||||
|
expect(name.previousValue).toEqual(undefined);
|
||||||
|
expect(name.currentValue).toEqual('value');
|
||||||
|
expect(name.firstChange).toEqual(true);
|
||||||
|
events.push('ngOnChanges');
|
||||||
|
};
|
||||||
|
ChildPrototype.ngDoCheck = () => events.push('ngDoCheck');
|
||||||
|
ChildPrototype.ngAfterContentInit = () => events.push('ngAfterContentInit');
|
||||||
|
ChildPrototype.ngAfterContentChecked = () => events.push('ngAfterContentChecked');
|
||||||
|
ChildPrototype.ngAfterViewInit = () => events.push('ngAfterViewInit');
|
||||||
|
ChildPrototype.ngAfterViewChecked = () => events.push('ngAfterViewChecked');
|
||||||
|
ChildPrototype.ngOnDestroy = () => events.push('ngOnDestroy');
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Child],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
fixture.destroy();
|
||||||
|
expect(events).toEqual([
|
||||||
|
'ngOnChanges', 'onInit', 'ngDoCheck', 'ngAfterContentInit', 'ngAfterContentChecked',
|
||||||
|
'ngAfterViewInit', 'ngAfterViewChecked', 'ngOnDestroy'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow adding lifecycle hook methods with inheritance any time before first instance creation',
|
||||||
|
() => {
|
||||||
|
const events: any[] = [];
|
||||||
|
|
||||||
|
@Component({template: `<child name="value"></child>`})
|
||||||
|
class App {
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseChild {}
|
||||||
|
|
||||||
|
@Component({selector: 'child', template: `empty`})
|
||||||
|
class Child extends BaseChild {
|
||||||
|
@Input() name: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are defined on the base class
|
||||||
|
const BasePrototype = BaseChild.prototype as any;
|
||||||
|
BasePrototype.ngOnInit = () => events.push('onInit');
|
||||||
|
BasePrototype.ngOnChanges = (e: SimpleChanges) => {
|
||||||
|
const name = e['name'];
|
||||||
|
expect(name.previousValue).toEqual(undefined);
|
||||||
|
expect(name.currentValue).toEqual('value');
|
||||||
|
expect(name.firstChange).toEqual(true);
|
||||||
|
events.push('ngOnChanges');
|
||||||
|
};
|
||||||
|
|
||||||
|
// These will be overwritten later
|
||||||
|
BasePrototype.ngDoCheck = () => events.push('Expected to be overbidden');
|
||||||
|
BasePrototype.ngAfterContentInit = () => events.push('Expected to be overbidden');
|
||||||
|
|
||||||
|
|
||||||
|
// These are define on the concrete class
|
||||||
|
const ChildPrototype = Child.prototype as any;
|
||||||
|
ChildPrototype.ngDoCheck = () => events.push('ngDoCheck');
|
||||||
|
ChildPrototype.ngAfterContentInit = () => events.push('ngAfterContentInit');
|
||||||
|
ChildPrototype.ngAfterContentChecked = () => events.push('ngAfterContentChecked');
|
||||||
|
ChildPrototype.ngAfterViewInit = () => events.push('ngAfterViewInit');
|
||||||
|
ChildPrototype.ngAfterViewChecked = () => events.push('ngAfterViewChecked');
|
||||||
|
ChildPrototype.ngOnDestroy = () => events.push('ngOnDestroy');
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [App, Child],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
fixture.destroy();
|
||||||
|
expect(events).toEqual([
|
||||||
|
'ngOnChanges', 'onInit', 'ngDoCheck', 'ngAfterContentInit', 'ngAfterContentChecked',
|
||||||
|
'ngAfterViewInit', 'ngAfterViewChecked', 'ngOnDestroy'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should call all hooks in correct order when several directives on same node', () => {
|
it('should call all hooks in correct order when several directives on same node', () => {
|
||||||
let log: string[] = [];
|
let log: string[] = [];
|
||||||
|
|
||||||
|
|
|
@ -200,6 +200,9 @@
|
||||||
{
|
{
|
||||||
"name": "getPreviousOrParentTNode"
|
"name": "getPreviousOrParentTNode"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getSimpleChangesStore"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getTView"
|
"name": "getTView"
|
||||||
},
|
},
|
||||||
|
@ -281,6 +284,9 @@
|
||||||
{
|
{
|
||||||
"name": "nextNgElementId"
|
"name": "nextNgElementId"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ngOnChangesSetInput"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "noSideEffects"
|
"name": "noSideEffects"
|
||||||
},
|
},
|
||||||
|
@ -293,6 +299,9 @@
|
||||||
{
|
{
|
||||||
"name": "refreshView"
|
"name": "refreshView"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "rememberChangeHistoryAndInvokeOnChangesHook"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "renderComponent"
|
"name": "renderComponent"
|
||||||
},
|
},
|
||||||
|
|
|
@ -137,6 +137,9 @@
|
||||||
{
|
{
|
||||||
"name": "getPreviousOrParentTNode"
|
"name": "getPreviousOrParentTNode"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getSimpleChangesStore"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "includeViewProviders"
|
"name": "includeViewProviders"
|
||||||
},
|
},
|
||||||
|
@ -176,6 +179,9 @@
|
||||||
{
|
{
|
||||||
"name": "nextNgElementId"
|
"name": "nextNgElementId"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ngOnChangesSetInput"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "refreshComponent"
|
"name": "refreshComponent"
|
||||||
},
|
},
|
||||||
|
@ -185,6 +191,9 @@
|
||||||
{
|
{
|
||||||
"name": "refreshView"
|
"name": "refreshView"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "rememberChangeHistoryAndInvokeOnChangesHook"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "renderComponent"
|
"name": "renderComponent"
|
||||||
},
|
},
|
||||||
|
|
|
@ -392,6 +392,9 @@
|
||||||
{
|
{
|
||||||
"name": "getSelectedIndex"
|
"name": "getSelectedIndex"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getSimpleChangesStore"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getSymbolIterator"
|
"name": "getSymbolIterator"
|
||||||
},
|
},
|
||||||
|
@ -548,6 +551,9 @@
|
||||||
{
|
{
|
||||||
"name": "nextNgElementId"
|
"name": "nextNgElementId"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ngOnChangesSetInput"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "noSideEffects"
|
"name": "noSideEffects"
|
||||||
},
|
},
|
||||||
|
@ -569,6 +575,9 @@
|
||||||
{
|
{
|
||||||
"name": "registerPostOrderHooks"
|
"name": "registerPostOrderHooks"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "rememberChangeHistoryAndInvokeOnChangesHook"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "removeFromArray"
|
"name": "removeFromArray"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue