fix(ivy): ngOnChanges only runs for binding updates (#27965)
PR Close #27965
This commit is contained in:
@ -11,8 +11,7 @@ import {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
|
||||
import {IterableDifferFactory, IterableDiffers} from './differs/iterable_differs';
|
||||
import {KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
|
||||
|
||||
export {SimpleChanges} from '../metadata/lifecycle_hooks';
|
||||
export {SimpleChange, WrappedValue, devModeEqual} from './change_detection_util';
|
||||
export {WrappedValue, devModeEqual} from './change_detection_util';
|
||||
export {ChangeDetectorRef} from './change_detector_ref';
|
||||
export {ChangeDetectionStrategy, ChangeDetectorStatus, isDefaultChangeDetectionStrategy} from './constants';
|
||||
export {DefaultIterableDifferFactory} from './differs/default_iterable_differ';
|
||||
@ -21,6 +20,7 @@ export {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
|
||||
export {CollectionChangeRecord, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs';
|
||||
export {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
|
||||
export {PipeTransform} from './pipe_transform';
|
||||
export {SimpleChange, SimpleChanges} from './simple_change';
|
||||
|
||||
|
||||
|
||||
|
@ -64,20 +64,6 @@ export class WrappedValue {
|
||||
static isWrapped(value: any): value is WrappedValue { return value instanceof WrappedValue; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a basic change from a previous to a new value.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export class SimpleChange {
|
||||
constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {}
|
||||
|
||||
/**
|
||||
* Check whether the new value is the first value assigned.
|
||||
*/
|
||||
isFirstChange(): boolean { return this.firstChange; }
|
||||
}
|
||||
|
||||
export function isListLikeIterable(obj: any): boolean {
|
||||
if (!isJsObject(obj)) return false;
|
||||
return Array.isArray(obj) ||
|
||||
|
35
packages/core/src/change_detection/simple_change.ts
Normal file
35
packages/core/src/change_detection/simple_change.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a basic change from a previous to a new value for a single
|
||||
* property on a directive instance. Passed as a value in a
|
||||
* {@link SimpleChanges} object to the `ngOnChanges` hook.
|
||||
*
|
||||
* @see `OnChanges`
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export class SimpleChange {
|
||||
constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {}
|
||||
/**
|
||||
* Check whether the new value is the first value assigned.
|
||||
*/
|
||||
isFirstChange(): boolean { return this.firstChange; }
|
||||
}
|
||||
|
||||
/**
|
||||
* A hashtable of changes represented by {@link SimpleChange} objects stored
|
||||
* at the declared property name they belong to on a Directive or Component. This is
|
||||
* the type passed to the `ngOnChanges` hook.
|
||||
*
|
||||
* @see `OnChanges`
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface SimpleChanges { [propName: string]: SimpleChange; }
|
@ -28,7 +28,6 @@ export {
|
||||
templateRefExtractor as ɵtemplateRefExtractor,
|
||||
ProvidersFeature as ɵProvidersFeature,
|
||||
InheritDefinitionFeature as ɵInheritDefinitionFeature,
|
||||
NgOnChangesFeature as ɵNgOnChangesFeature,
|
||||
LifecycleHooksFeature as ɵLifecycleHooksFeature,
|
||||
NgModuleType as ɵNgModuleType,
|
||||
NgModuleRef as ɵRender3NgModuleRef,
|
||||
|
@ -9,13 +9,12 @@
|
||||
import {ChangeDetectionStrategy} from '../change_detection/constants';
|
||||
import {Provider} from '../di';
|
||||
import {Type} from '../interface/type';
|
||||
import {NG_BASE_DEF} from '../render3/fields';
|
||||
import {NG_BASE_DEF, NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../render3/fields';
|
||||
import {compileComponent as render3CompileComponent, compileDirective as render3CompileDirective} from '../render3/jit/directive';
|
||||
import {compilePipe as render3CompilePipe} from '../render3/jit/pipe';
|
||||
import {TypeDecorator, makeDecorator, makePropDecorator} from '../util/decorators';
|
||||
import {noop} from '../util/noop';
|
||||
import {fillProperties} from '../util/property';
|
||||
|
||||
import {ViewEncapsulation} from './view';
|
||||
|
||||
|
||||
@ -715,21 +714,46 @@ const initializeBaseDef = (target: any): void => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Does the work of creating the `ngBaseDef` property for the @Input and @Output decorators.
|
||||
* @param key "inputs" or "outputs"
|
||||
* Returns a function that will update the static definition on a class to have the
|
||||
* appropriate input or output mapping.
|
||||
*
|
||||
* Will also add an {@link ngBaseDef} property to a directive if no `ngDirectiveDef`
|
||||
* or `ngComponentDef` is present. This is done because a class may have {@link InputDecorator}s and
|
||||
* {@link OutputDecorator}s without having a {@link ComponentDecorator} or {@link DirectiveDecorator},
|
||||
* and those inputs and outputs should still be inheritable, we need to add an
|
||||
* `ngBaseDef` property if there are no existing `ngComponentDef` or `ngDirectiveDef`
|
||||
* properties, so that we can track the inputs and outputs for inheritance purposes.
|
||||
*
|
||||
* @param getPropertyToUpdate A function that maps to either the `inputs` property or the
|
||||
* `outputs` property of a definition.
|
||||
* @returns A function that, the called, will add a `ngBaseDef` if no other definition is present,
|
||||
* then update the `inputs` or `outputs` on it, depending on what was selected by `getPropertyToUpdate`
|
||||
*
|
||||
*
|
||||
* @see InputDecorator
|
||||
* @see OutputDecorator
|
||||
* @see InheritenceFeature
|
||||
*/
|
||||
const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any}) => any) =>
|
||||
(target: any, name: string, ...args: any[]) => {
|
||||
const constructor = target.constructor;
|
||||
function getOrCreateDefinitionAndUpdateMappingFor(
|
||||
getPropertyToUpdate: (baseDef: {inputs?: any, outputs?: any}) => any) {
|
||||
return function updateIOProp(target: any, name: string, ...args: any[]) {
|
||||
const constructor = target.constructor;
|
||||
|
||||
if (!constructor.hasOwnProperty(NG_BASE_DEF)) {
|
||||
initializeBaseDef(target);
|
||||
}
|
||||
let def: any =
|
||||
constructor[NG_COMPONENT_DEF] || constructor[NG_DIRECTIVE_DEF] || constructor[NG_BASE_DEF];
|
||||
|
||||
const baseDef = constructor.ngBaseDef;
|
||||
const defProp = getProp(baseDef);
|
||||
if (!def) {
|
||||
initializeBaseDef(target);
|
||||
def = constructor[NG_BASE_DEF];
|
||||
}
|
||||
|
||||
const defProp = getPropertyToUpdate(def);
|
||||
// Use of `in` because we *do* want to check the prototype chain here.
|
||||
if (!(name in defProp)) {
|
||||
defProp[name] = args[0];
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
@ -737,7 +761,7 @@ const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any
|
||||
*/
|
||||
export const Input: InputDecorator = makePropDecorator(
|
||||
'Input', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined,
|
||||
updateBaseDefFromIOProp(baseDef => baseDef.inputs || {}));
|
||||
getOrCreateDefinitionAndUpdateMappingFor(def => def.inputs || {}));
|
||||
|
||||
/**
|
||||
* Type of the Output decorator / constructor function.
|
||||
@ -777,7 +801,7 @@ export interface Output { bindingPropertyName?: string; }
|
||||
*/
|
||||
export const Output: OutputDecorator = makePropDecorator(
|
||||
'Output', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined,
|
||||
updateBaseDefFromIOProp(baseDef => baseDef.outputs || {}));
|
||||
getOrCreateDefinitionAndUpdateMappingFor(def => def.outputs || {}));
|
||||
|
||||
|
||||
|
||||
|
@ -5,19 +5,8 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {SimpleChanges} from '../change_detection/simple_change';
|
||||
|
||||
import {SimpleChange} from '../change_detection/change_detection_util';
|
||||
|
||||
|
||||
/**
|
||||
* Defines an object that associates properties with
|
||||
* instances of `SimpleChange`.
|
||||
*
|
||||
* @see `OnChanges`
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface SimpleChanges { [propName: string]: SimpleChange; }
|
||||
|
||||
/**
|
||||
* @description
|
||||
|
@ -17,7 +17,7 @@ import {assertComponentType} from './assert';
|
||||
import {getComponentDef} from './definition';
|
||||
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
|
||||
import {publishDefaultGlobalUtils} from './global_utils';
|
||||
import {queueInitHooks, queueLifecycleHooks} from './hooks';
|
||||
import {registerPostOrderHooks, registerPreOrderHooks} from './hooks';
|
||||
import {CLEAN_PROMISE, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions';
|
||||
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
|
||||
import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
|
||||
@ -237,10 +237,11 @@ export function LifecycleHooksFeature(component: any, def: ComponentDef<any>): v
|
||||
const rootTView = readPatchedLView(component) ![TVIEW];
|
||||
const dirIndex = rootTView.data.length - 1;
|
||||
|
||||
queueInitHooks(dirIndex, def.onInit, def.doCheck, rootTView);
|
||||
registerPreOrderHooks(dirIndex, def, rootTView);
|
||||
// TODO(misko): replace `as TNode` with createTNode call. (needs refactoring to lose dep on
|
||||
// LNode).
|
||||
queueLifecycleHooks(rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
|
||||
registerPostOrderHooks(
|
||||
rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -12,6 +12,7 @@ import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
|
||||
import {TNode, TNodeFlags} from './interfaces/node';
|
||||
import {RElement} from './interfaces/renderer';
|
||||
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view';
|
||||
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
|
||||
import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util';
|
||||
|
||||
|
||||
@ -257,7 +258,7 @@ function findViaDirective(lView: LView, directiveInstance: {}): number {
|
||||
const directiveIndexStart = tNode.directiveStart;
|
||||
const directiveIndexEnd = tNode.directiveEnd;
|
||||
for (let i = directiveIndexStart; i < directiveIndexEnd; i++) {
|
||||
if (lView[i] === directiveInstance) {
|
||||
if (unwrapOnChangesDirectiveWrapper(lView[i]) === directiveInstance) {
|
||||
return tNode.index;
|
||||
}
|
||||
}
|
||||
|
@ -202,7 +202,7 @@ export function defineComponent<T>(componentDefinition: {
|
||||
/**
|
||||
* A list of optional features to apply.
|
||||
*
|
||||
* See: {@link NgOnChangesFeature}, {@link ProvidersFeature}
|
||||
* See: {@link ProvidersFeature}
|
||||
*/
|
||||
features?: ComponentDefFeature[];
|
||||
|
||||
@ -265,6 +265,7 @@ export function defineComponent<T>(componentDefinition: {
|
||||
inputs: null !, // assigned in noSideEffects
|
||||
outputs: null !, // assigned in noSideEffects
|
||||
exportAs: componentDefinition.exportAs || null,
|
||||
onChanges: typePrototype.ngOnChanges || null,
|
||||
onInit: typePrototype.ngOnInit || null,
|
||||
doCheck: typePrototype.ngDoCheck || null,
|
||||
afterContentInit: typePrototype.ngAfterContentInit || null,
|
||||
@ -583,7 +584,7 @@ export const defineDirective = defineComponent as any as<T>(directiveDefinition:
|
||||
/**
|
||||
* A list of optional features to apply.
|
||||
*
|
||||
* See: {@link NgOnChangesFeature}, {@link ProvidersFeature}, {@link InheritDefinitionFeature}
|
||||
* See: {@link ProvidersFeature}, {@link InheritDefinitionFeature}
|
||||
*/
|
||||
features?: DirectiveDefFeature[];
|
||||
|
||||
|
@ -20,6 +20,7 @@ import {NO_PARENT_INJECTOR, NodeInjectorFactory, PARENT_INJECTOR, RelativeInject
|
||||
import {AttributeMarker, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType} from './interfaces/node';
|
||||
import {DECLARATION_VIEW, HOST_NODE, INJECTOR, LView, TData, TVIEW, TView} from './interfaces/view';
|
||||
import {assertNodeOfPossibleTypes} from './node_assert';
|
||||
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
|
||||
import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state';
|
||||
import {findComponentView, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, stringify} from './util';
|
||||
|
||||
@ -514,6 +515,8 @@ export function getNodeInjectable(
|
||||
factory.resolving = false;
|
||||
setTNodeAndViewData(savePreviousOrParentTNode, saveLView);
|
||||
}
|
||||
} else {
|
||||
value = unwrapOnChangesDirectiveWrapper(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
@ -35,8 +35,6 @@ function getSuperType(type: Type<any>): Type<any>&
|
||||
export function InheritDefinitionFeature(definition: DirectiveDef<any>| ComponentDef<any>): void {
|
||||
let superType = getSuperType(definition.type);
|
||||
|
||||
debugger;
|
||||
|
||||
while (superType) {
|
||||
let superDef: DirectiveDef<any>|ComponentDef<any>|undefined = undefined;
|
||||
if (isComponentDef(definition)) {
|
||||
@ -62,7 +60,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
|
||||
}
|
||||
|
||||
if (baseDef) {
|
||||
// Merge inputs and outputs
|
||||
fillProperties(definition.inputs, baseDef.inputs);
|
||||
fillProperties(definition.declaredInputs, baseDef.declaredInputs);
|
||||
fillProperties(definition.outputs, baseDef.outputs);
|
||||
@ -127,7 +124,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Merge inputs and outputs
|
||||
fillProperties(definition.inputs, superDef.inputs);
|
||||
fillProperties(definition.declaredInputs, superDef.declaredInputs);
|
||||
@ -143,6 +139,7 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
|
||||
definition.doCheck = definition.doCheck || superDef.doCheck;
|
||||
definition.onDestroy = definition.onDestroy || superDef.onDestroy;
|
||||
definition.onInit = definition.onInit || superDef.onInit;
|
||||
definition.onChanges = definition.onChanges || superDef.onChanges;
|
||||
|
||||
// Run parent features
|
||||
const features = superDef.features;
|
||||
|
@ -1,124 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {SimpleChange} from '../../change_detection/change_detection_util';
|
||||
import {OnChanges, SimpleChanges} from '../../metadata/lifecycle_hooks';
|
||||
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
|
||||
* lifecycle hook, so it should be included in any component that implements
|
||||
* that hook.
|
||||
*
|
||||
* If the component or directive uses inheritance, the NgOnChangesFeature MUST
|
||||
* be included as a feature AFTER {@link InheritDefinitionFeature}, otherwise
|
||||
* inherited properties will not be propagated to the ngOnChanges lifecycle
|
||||
* hook.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```
|
||||
* static ngComponentDef = defineComponent({
|
||||
* ...
|
||||
* inputs: {name: 'publicName'},
|
||||
* features: [NgOnChangesFeature]
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function NgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
|
||||
const publicToDeclaredInputs = definition.declaredInputs;
|
||||
const publicToMinifiedInputs = definition.inputs;
|
||||
const proto = definition.type.prototype;
|
||||
for (const publicName in publicToDeclaredInputs) {
|
||||
if (publicToDeclaredInputs.hasOwnProperty(publicName)) {
|
||||
const minifiedKey = publicToMinifiedInputs[publicName];
|
||||
const declaredKey = publicToDeclaredInputs[publicName];
|
||||
const privateMinKey = PRIVATE_PREFIX + minifiedKey;
|
||||
|
||||
// Walk the prototype chain to see if we find a property descriptor
|
||||
// That way we can honor setters and getters that were inherited.
|
||||
let originalProperty: PropertyDescriptor|undefined = undefined;
|
||||
let checkProto = proto;
|
||||
while (!originalProperty && checkProto &&
|
||||
Object.getPrototypeOf(checkProto) !== Object.getPrototypeOf(Object.prototype)) {
|
||||
originalProperty = Object.getOwnPropertyDescriptor(checkProto, minifiedKey);
|
||||
checkProto = Object.getPrototypeOf(checkProto);
|
||||
}
|
||||
|
||||
const getter = originalProperty && originalProperty.get;
|
||||
const setter = originalProperty && originalProperty.set;
|
||||
|
||||
// create a getter and setter for property
|
||||
Object.defineProperty(proto, minifiedKey, {
|
||||
get: getter ||
|
||||
(setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }),
|
||||
set<T>(this: OnChangesExpando, value: T) {
|
||||
let simpleChanges = this[PRIVATE_PREFIX];
|
||||
if (!simpleChanges) {
|
||||
simpleChanges = {};
|
||||
// Place where we will store SimpleChanges if there is a change
|
||||
Object.defineProperty(this, PRIVATE_PREFIX, {value: simpleChanges, writable: true});
|
||||
}
|
||||
|
||||
const isFirstChange = !this.hasOwnProperty(privateMinKey);
|
||||
const currentChange = simpleChanges[declaredKey];
|
||||
|
||||
if (currentChange) {
|
||||
currentChange.currentValue = value;
|
||||
} else {
|
||||
simpleChanges[declaredKey] =
|
||||
new SimpleChange(this[privateMinKey], value, isFirstChange);
|
||||
}
|
||||
|
||||
if (isFirstChange) {
|
||||
// Create a place where the actual value will be stored and make it non-enumerable
|
||||
Object.defineProperty(this, privateMinKey, {value, writable: true});
|
||||
} else {
|
||||
this[privateMinKey] = value;
|
||||
}
|
||||
|
||||
if (setter) setter.call(this, value);
|
||||
},
|
||||
// Make the property configurable in dev mode to allow overriding in tests
|
||||
configurable: !!ngDevMode
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If an onInit hook is defined, it will need to wrap the ngOnChanges call
|
||||
// so the call order is changes-init-check in creation mode. In subsequent
|
||||
// change detection runs, only the check wrapper will be called.
|
||||
if (definition.onInit != null) {
|
||||
definition.onInit = onChangesWrapper(definition.onInit);
|
||||
}
|
||||
|
||||
definition.doCheck = onChangesWrapper(definition.doCheck);
|
||||
}
|
||||
|
||||
// This option ensures that the ngOnChanges lifecycle hook will be inherited
|
||||
// from superclasses (in InheritDefinitionFeature).
|
||||
(NgOnChangesFeature as DirectiveDefFeature).ngInherit = true;
|
||||
|
||||
function onChangesWrapper(delegateHook: (() => void) | null) {
|
||||
return function(this: OnChangesExpando) {
|
||||
const simpleChanges = this[PRIVATE_PREFIX];
|
||||
if (simpleChanges != null) {
|
||||
this.ngOnChanges(simpleChanges);
|
||||
this[PRIVATE_PREFIX] = null;
|
||||
}
|
||||
if (delegateHook) delegateHook.apply(this);
|
||||
};
|
||||
}
|
@ -6,92 +6,117 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {SimpleChanges} from '../change_detection/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 {OnChangesDirectiveWrapper, unwrapOnChangesDirectiveWrapper} from './onchanges_util';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* If this is the first template pass, any ngOnInit or ngDoCheck hooks will be queued into
|
||||
* TView.initHooks during directiveCreate.
|
||||
* Adds all directive lifecycle hooks from the given `DirectiveDef` to the given `TView`.
|
||||
*
|
||||
* The directive index and hook type are encoded into one number (1st bit: type, remaining bits:
|
||||
* directive index), then saved in the even indices of the initHooks array. The odd indices
|
||||
* hold the hook functions themselves.
|
||||
* Must be run *only* on the first template pass.
|
||||
*
|
||||
* @param index The index of the directive in LView
|
||||
* @param hooks The static hooks map on the directive def
|
||||
* The TView's hooks arrays are arranged in alternating pairs of directiveIndex and hookFunction,
|
||||
* i.e.: `[directiveIndexA, hookFunctionA, directiveIndexB, hookFunctionB, ...]`. For `OnChanges`
|
||||
* hooks, the `directiveIndex` will be *negative*, signaling {@link callHooks} that the
|
||||
* `hookFunction` must be passed the the appropriate {@link SimpleChanges} object.
|
||||
*
|
||||
* @param directiveIndex The index of the directive in LView
|
||||
* @param directiveDef The definition containing the hooks to setup in tView
|
||||
* @param tView The current TView
|
||||
*/
|
||||
export function queueInitHooks(
|
||||
index: number, onInit: (() => void) | null, doCheck: (() => void) | null, tView: TView): void {
|
||||
export function registerPreOrderHooks(
|
||||
directiveIndex: number, directiveDef: DirectiveDef<any>, tView: TView): void {
|
||||
ngDevMode &&
|
||||
assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass');
|
||||
|
||||
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(index, onInit);
|
||||
(tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onInit);
|
||||
}
|
||||
|
||||
if (doCheck) {
|
||||
(tView.initHooks || (tView.initHooks = [])).push(index, doCheck);
|
||||
(tView.checkHooks || (tView.checkHooks = [])).push(index, doCheck);
|
||||
(tView.initHooks || (tView.initHooks = [])).push(directiveIndex, doCheck);
|
||||
(tView.checkHooks || (tView.checkHooks = [])).push(directiveIndex, doCheck);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops through the directives on a node and queues all their hooks except ngOnInit
|
||||
* and ngDoCheck, which are queued separately in directiveCreate.
|
||||
*
|
||||
* Loops through the directives on the provided `tNode` and queues hooks to be
|
||||
* run that are not initialization hooks.
|
||||
*
|
||||
* Should be executed during `elementEnd()` and similar to
|
||||
* preserve hook execution order. Content, view, and destroy hooks for projected
|
||||
* components and directives must be called *before* their hosts.
|
||||
*
|
||||
* Sets up the content, view, and destroy hooks on the provided `tView` such that
|
||||
* they're added in alternating pairs of directiveIndex and hookFunction,
|
||||
* i.e.: `[directiveIndexA, hookFunctionA, directiveIndexB, hookFunctionB, ...]`
|
||||
*
|
||||
* NOTE: This does not set up `onChanges`, `onInit` or `doCheck`, those are set up
|
||||
* separately at `elementStart`.
|
||||
*
|
||||
* @param tView The current TView
|
||||
* @param tNode The TNode whose directives are to be searched for hooks to queue
|
||||
*/
|
||||
export function queueLifecycleHooks(tView: TView, tNode: TNode): void {
|
||||
export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
|
||||
if (tView.firstTemplatePass) {
|
||||
// It's necessary to loop through the directives at elementEnd() (rather than processing in
|
||||
// directiveCreate) so we can preserve the current hook order. Content, view, and destroy
|
||||
// hooks for projected components and directives must be called *before* their hosts.
|
||||
for (let i = tNode.directiveStart, end = tNode.directiveEnd; i < end; i++) {
|
||||
const def = tView.data[i] as DirectiveDef<any>;
|
||||
queueContentHooks(def, tView, i);
|
||||
queueViewHooks(def, tView, i);
|
||||
queueDestroyHooks(def, tView, i);
|
||||
const directiveDef = tView.data[i] as DirectiveDef<any>;
|
||||
if (directiveDef.afterContentInit) {
|
||||
(tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentInit);
|
||||
}
|
||||
|
||||
if (directiveDef.afterContentChecked) {
|
||||
(tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentChecked);
|
||||
(tView.contentCheckHooks || (tView.contentCheckHooks = [
|
||||
])).push(i, directiveDef.afterContentChecked);
|
||||
}
|
||||
|
||||
if (directiveDef.afterViewInit) {
|
||||
(tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewInit);
|
||||
}
|
||||
|
||||
if (directiveDef.afterViewChecked) {
|
||||
(tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewChecked);
|
||||
(tView.viewCheckHooks || (tView.viewCheckHooks = [
|
||||
])).push(i, directiveDef.afterViewChecked);
|
||||
}
|
||||
|
||||
if (directiveDef.onDestroy != null) {
|
||||
(tView.destroyHooks || (tView.destroyHooks = [])).push(i, directiveDef.onDestroy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Queues afterContentInit and afterContentChecked hooks on TView */
|
||||
function queueContentHooks(def: DirectiveDef<any>, tView: TView, i: number): void {
|
||||
if (def.afterContentInit) {
|
||||
(tView.contentHooks || (tView.contentHooks = [])).push(i, def.afterContentInit);
|
||||
}
|
||||
|
||||
if (def.afterContentChecked) {
|
||||
(tView.contentHooks || (tView.contentHooks = [])).push(i, def.afterContentChecked);
|
||||
(tView.contentCheckHooks || (tView.contentCheckHooks = [])).push(i, def.afterContentChecked);
|
||||
}
|
||||
}
|
||||
|
||||
/** Queues afterViewInit and afterViewChecked hooks on TView */
|
||||
function queueViewHooks(def: DirectiveDef<any>, tView: TView, i: number): void {
|
||||
if (def.afterViewInit) {
|
||||
(tView.viewHooks || (tView.viewHooks = [])).push(i, def.afterViewInit);
|
||||
}
|
||||
|
||||
if (def.afterViewChecked) {
|
||||
(tView.viewHooks || (tView.viewHooks = [])).push(i, def.afterViewChecked);
|
||||
(tView.viewCheckHooks || (tView.viewCheckHooks = [])).push(i, def.afterViewChecked);
|
||||
}
|
||||
}
|
||||
|
||||
/** Queues onDestroy hooks on TView */
|
||||
function queueDestroyHooks(def: DirectiveDef<any>, tView: TView, i: number): void {
|
||||
if (def.onDestroy != null) {
|
||||
(tView.destroyHooks || (tView.destroyHooks = [])).push(i, def.onDestroy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls onInit and doCheck calls if they haven't already been called.
|
||||
* Executes necessary hooks at the start of executing a template.
|
||||
*
|
||||
* @param currentView The current view
|
||||
* Executes hooks that are to be run during the initialization of a directive such
|
||||
* as `onChanges`, `onInit`, and `doCheck`.
|
||||
*
|
||||
* Has the side effect of updating the RunInit flag in `lView` to be `0`, so that
|
||||
* this isn't run a second time.
|
||||
*
|
||||
* @param lView The current view
|
||||
* @param tView Static data for the view containing the hooks to be executed
|
||||
* @param creationMode Whether or not we're in creation mode.
|
||||
*/
|
||||
export function executeInitHooks(
|
||||
currentView: LView, tView: TView, checkNoChangesMode: boolean): void {
|
||||
@ -102,16 +127,20 @@ export function executeInitHooks(
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over afterViewInit and afterViewChecked functions and calls them.
|
||||
* Executes hooks against the given `LView` based off of whether or not
|
||||
* This is the first pass.
|
||||
*
|
||||
* @param currentView The current view
|
||||
* @param lView The view instance data to run the hooks against
|
||||
* @param firstPassHooks An array of hooks to run if we're in the first view pass
|
||||
* @param checkHooks An Array of hooks to run if we're not in the first view pass.
|
||||
* @param checkNoChangesMode Whether or not we're in no changes mode.
|
||||
*/
|
||||
export function executeHooks(
|
||||
currentView: LView, allHooks: HookData | null, checkHooks: HookData | null,
|
||||
currentView: LView, firstPassHooks: HookData | null, checkHooks: HookData | null,
|
||||
checkNoChangesMode: boolean): void {
|
||||
if (checkNoChangesMode) return;
|
||||
|
||||
const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? allHooks : checkHooks;
|
||||
const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? firstPassHooks : checkHooks;
|
||||
if (hooksToCall) {
|
||||
callHooks(currentView, hooksToCall);
|
||||
}
|
||||
@ -119,13 +148,31 @@ export function executeHooks(
|
||||
|
||||
/**
|
||||
* Calls lifecycle hooks with their contexts, skipping init hooks if it's not
|
||||
* the first LView pass.
|
||||
* the first LView pass, and skipping onChanges hooks if there are no changes present.
|
||||
*
|
||||
* @param currentView The current view
|
||||
* @param arr The array in which the hooks are found
|
||||
*/
|
||||
export function callHooks(currentView: any[], arr: HookData): void {
|
||||
export function callHooks(currentView: LView, arr: HookData): void {
|
||||
for (let i = 0; i < arr.length; i += 2) {
|
||||
(arr[i + 1] as() => void).call(currentView[arr[i] as number]);
|
||||
const directiveIndex = arr[i] as number;
|
||||
const hook = arr[i + 1] as((() => void) | ((changes: SimpleChanges) => void));
|
||||
// Negative indices signal that we're dealing with an `onChanges` hook.
|
||||
const isOnChangesHook = directiveIndex < 0;
|
||||
const directiveOrWrappedDirective =
|
||||
currentView[isOnChangesHook ? -directiveIndex : directiveIndex];
|
||||
const directive = unwrapOnChangesDirectiveWrapper(directiveOrWrappedDirective);
|
||||
|
||||
if (isOnChangesHook) {
|
||||
const onChanges: OnChangesDirectiveWrapper = directiveOrWrappedDirective;
|
||||
const changes = onChanges.changes;
|
||||
if (changes) {
|
||||
onChanges.previous = changes;
|
||||
onChanges.changes = null;
|
||||
hook.call(onChanges.instance, changes);
|
||||
}
|
||||
} else {
|
||||
hook.call(directive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import {LifecycleHooksFeature, renderComponent, whenRendered} from './component'
|
||||
import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe} from './definition';
|
||||
import {getComponent, getHostElement, getRenderedText} from './discovery_utils';
|
||||
import {InheritDefinitionFeature} from './features/inherit_definition_feature';
|
||||
import {NgOnChangesFeature} from './features/ng_onchanges_feature';
|
||||
import {ProvidersFeature} from './features/providers_feature';
|
||||
import {BaseDef, ComponentDef, ComponentDefWithMeta, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefWithMeta, DirectiveType, PipeDef, PipeDefWithMeta} from './interfaces/definition';
|
||||
|
||||
@ -158,7 +157,6 @@ export {
|
||||
DirectiveDefFlags,
|
||||
DirectiveDefWithMeta,
|
||||
DirectiveType,
|
||||
NgOnChangesFeature,
|
||||
InheritDefinitionFeature,
|
||||
ProvidersFeature,
|
||||
PipeDef,
|
||||
|
@ -22,7 +22,7 @@ import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from
|
||||
import {attachPatchData, getComponentViewByInstance} from './context_discovery';
|
||||
import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreateNodeInjectorForNode, injectAttributeImpl} from './di';
|
||||
import {throwMultipleComponentError} from './errors';
|
||||
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
|
||||
import {executeHooks, executeInitHooks, registerPostOrderHooks, registerPreOrderHooks} from './hooks';
|
||||
import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container';
|
||||
import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
|
||||
import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from './interfaces/injector';
|
||||
@ -36,6 +36,7 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLA
|
||||
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
|
||||
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
|
||||
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
|
||||
import {OnChangesDirectiveWrapper, isOnChangesDirectiveWrapper, recordChange, unwrapOnChangesDirectiveWrapper} from './onchanges_util';
|
||||
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state';
|
||||
import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
|
||||
import {BoundPlayerFactory} from './styling/player_factory';
|
||||
@ -120,7 +121,7 @@ export function setHostBindings(tView: TView, viewData: LView): void {
|
||||
if (instruction !== null) {
|
||||
viewData[BINDING_INDEX] = bindingRootIndex;
|
||||
instruction(
|
||||
RenderFlags.Update, readElementValue(viewData[currentDirectiveIndex]),
|
||||
RenderFlags.Update, unwrapOnChangesDirectiveWrapper(viewData[currentDirectiveIndex]),
|
||||
currentElementIndex);
|
||||
}
|
||||
currentDirectiveIndex++;
|
||||
@ -522,7 +523,7 @@ export function elementContainerEnd(): void {
|
||||
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TElementContainerNode);
|
||||
}
|
||||
|
||||
queueLifecycleHooks(tView, previousOrParentTNode);
|
||||
registerPostOrderHooks(tView, previousOrParentTNode);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -719,6 +720,7 @@ export function createTView(
|
||||
expandoStartIndex: initialViewLength,
|
||||
expandoInstructions: null,
|
||||
firstTemplatePass: true,
|
||||
changesHooks: null,
|
||||
initHooks: null,
|
||||
checkHooks: null,
|
||||
contentHooks: null,
|
||||
@ -883,9 +885,14 @@ export function listener(
|
||||
const propsLength = props.length;
|
||||
if (propsLength) {
|
||||
const lCleanup = getCleanup(lView);
|
||||
for (let i = 0; i < propsLength; i += 2) {
|
||||
ngDevMode && assertDataInRange(lView, props[i] as number);
|
||||
const subscription = lView[props[i] as number][props[i + 1]].subscribe(listenerFn);
|
||||
// Subscribe to listeners for each output, and setup clean up for each.
|
||||
for (let i = 0; i < propsLength;) {
|
||||
const directiveIndex = props[i++] as number;
|
||||
const minifiedName = props[i++] as string;
|
||||
const declaredName = props[i++] as string;
|
||||
ngDevMode && assertDataInRange(lView, directiveIndex as number);
|
||||
const directive = unwrapOnChangesDirectiveWrapper(lView[directiveIndex]);
|
||||
const subscription = directive[minifiedName].subscribe(listenerFn);
|
||||
const idx = lCleanup.length;
|
||||
lCleanup.push(listenerFn, subscription);
|
||||
tCleanup && tCleanup.push(eventName, tNode.index, idx, -(idx + 1));
|
||||
@ -943,7 +950,7 @@ export function elementEnd(): void {
|
||||
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TElementNode);
|
||||
}
|
||||
|
||||
queueLifecycleHooks(getLView()[TVIEW], previousOrParentTNode);
|
||||
registerPostOrderHooks(getLView()[TVIEW], previousOrParentTNode);
|
||||
decreaseElementDepthCount();
|
||||
|
||||
// this is fired at the end of elementEnd because ALL of the stylingBindings code
|
||||
@ -952,7 +959,7 @@ export function elementEnd(): void {
|
||||
if (hasClassInput(previousOrParentTNode)) {
|
||||
const stylingContext = getStylingContext(previousOrParentTNode.index, lView);
|
||||
setInputsForProperty(
|
||||
lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext));
|
||||
lView, previousOrParentTNode.inputs !, 'class', getInitialClassNameValue(stylingContext));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1051,7 +1058,7 @@ function elementPropertyInternal<T>(
|
||||
let dataValue: PropertyAliasValue|undefined;
|
||||
if (!nativeOnly && (inputData = initializeTNodeInputs(tNode)) &&
|
||||
(dataValue = inputData[propName])) {
|
||||
setInputsForProperty(lView, dataValue, value);
|
||||
setInputsForProperty(lView, inputData, propName, value);
|
||||
if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET);
|
||||
if (ngDevMode) {
|
||||
if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) {
|
||||
@ -1121,22 +1128,35 @@ export function createTNode(
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of directive indices and minified input names, sets the
|
||||
* input properties on the corresponding directives.
|
||||
* Set the inputs of directives at the current node to corresponding value.
|
||||
*
|
||||
* @param lView the `LView` which contains the directives.
|
||||
* @param inputAliases mapping between the public "input" name and privately-known,
|
||||
* possibly minified, property names to write to.
|
||||
* @param publicName public binding name. (This is the `<div [publicName]=value>`)
|
||||
* @param value Value to set.
|
||||
*/
|
||||
function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: any): void {
|
||||
for (let i = 0; i < inputs.length; i += 2) {
|
||||
ngDevMode && assertDataInRange(lView, inputs[i] as number);
|
||||
lView[inputs[i] as number][inputs[i + 1]] = value;
|
||||
function setInputsForProperty(
|
||||
lView: LView, inputAliases: PropertyAliases, publicName: string, value: any): void {
|
||||
const inputs = inputAliases[publicName];
|
||||
for (let i = 0; i < inputs.length;) {
|
||||
const directiveIndex = inputs[i++] as number;
|
||||
const privateName = inputs[i++] as string;
|
||||
const declaredName = inputs[i++] as string;
|
||||
ngDevMode && assertDataInRange(lView, directiveIndex);
|
||||
recordChangeAndUpdateProperty(lView[directiveIndex], declaredName, privateName, value);
|
||||
}
|
||||
}
|
||||
|
||||
function setNgReflectProperties(
|
||||
lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue,
|
||||
value: any) {
|
||||
for (let i = 0; i < inputs.length; i += 2) {
|
||||
for (let i = 0; i < inputs.length;) {
|
||||
const directiveIndex = inputs[i++] as number;
|
||||
const privateName = inputs[i++] as string;
|
||||
const declaredName = inputs[i++] as string;
|
||||
const renderer = lView[RENDERER];
|
||||
const attrName = normalizeDebugBindingName(inputs[i + 1] as string);
|
||||
const attrName = normalizeDebugBindingName(privateName);
|
||||
const debugValue = normalizeDebugBindingValue(value);
|
||||
if (type === TNodeType.Element) {
|
||||
isProceduralRenderer(renderer) ?
|
||||
@ -1172,15 +1192,20 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
const directiveDef = defs[i] as DirectiveDef<any>;
|
||||
const propertyAliasMap: {[publicName: string]: string} =
|
||||
const publicToMinifiedNames: {[publicName: string]: string} =
|
||||
isInput ? directiveDef.inputs : directiveDef.outputs;
|
||||
for (let publicName in propertyAliasMap) {
|
||||
if (propertyAliasMap.hasOwnProperty(publicName)) {
|
||||
const publicToDeclaredNames: {[publicName: string]: string}|null =
|
||||
isInput ? directiveDef.declaredInputs : null;
|
||||
for (let publicName in publicToMinifiedNames) {
|
||||
if (publicToMinifiedNames.hasOwnProperty(publicName)) {
|
||||
propStore = propStore || {};
|
||||
const internalName = propertyAliasMap[publicName];
|
||||
const hasProperty = propStore.hasOwnProperty(publicName);
|
||||
hasProperty ? propStore[publicName].push(i, internalName) :
|
||||
(propStore[publicName] = [i, internalName]);
|
||||
const minifiedName = publicToMinifiedNames[publicName];
|
||||
const declaredName =
|
||||
publicToDeclaredNames ? publicToDeclaredNames[publicName] : minifiedName;
|
||||
const aliases: PropertyAliasValue = propStore.hasOwnProperty(publicName) ?
|
||||
propStore[publicName] :
|
||||
propStore[publicName] = [];
|
||||
aliases.push(i, minifiedName, declaredName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1382,7 +1407,7 @@ export function elementStylingMap<T>(
|
||||
const initialClasses = getInitialClassNameValue(stylingContext);
|
||||
const classInputVal =
|
||||
(initialClasses.length ? (initialClasses + ' ') : '') + (classes as string);
|
||||
setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal);
|
||||
setInputsForProperty(lView, tNode.inputs !, 'class', classInputVal);
|
||||
} else {
|
||||
updateStylingMap(stylingContext, classes, styles);
|
||||
}
|
||||
@ -1493,7 +1518,7 @@ function resolveDirectives(
|
||||
|
||||
// Init hooks are queued now so ngOnInit is called in host components before
|
||||
// any projected components.
|
||||
queueInitHooks(directiveDefIdx, def.onInit, def.doCheck, tView);
|
||||
registerPreOrderHooks(directiveDefIdx, def, tView);
|
||||
}
|
||||
}
|
||||
if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap);
|
||||
@ -1515,6 +1540,7 @@ function instantiateAllDirectives(tView: TView, lView: LView, tNode: TNode) {
|
||||
addComponentLogic(lView, tNode, def as ComponentDef<any>);
|
||||
}
|
||||
const directive = getNodeInjectable(tView.data, lView !, i, tNode as TElementNode);
|
||||
|
||||
postProcessDirective(lView, directive, def, i);
|
||||
}
|
||||
}
|
||||
@ -1526,7 +1552,7 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
|
||||
const firstTemplatePass = getFirstTemplatePass();
|
||||
for (let i = start; i < end; i++) {
|
||||
const def = tView.data[i] as DirectiveDef<any>;
|
||||
const directive = viewData[i];
|
||||
const directive = unwrapOnChangesDirectiveWrapper(viewData[i]);
|
||||
if (def.hostBindings) {
|
||||
const previousExpandoLength = expando.length;
|
||||
setCurrentDirectiveDef(def);
|
||||
@ -1583,12 +1609,17 @@ function prefillHostVars(tView: TView, lView: LView, totalHostVars: number): voi
|
||||
* Process a directive on the current node after its creation.
|
||||
*/
|
||||
function postProcessDirective<T>(
|
||||
viewData: LView, directive: T, def: DirectiveDef<T>, directiveDefIdx: number): void {
|
||||
lView: LView, directive: T, def: DirectiveDef<T>, directiveDefIdx: number): void {
|
||||
if (def.onChanges) {
|
||||
// We have onChanges, wrap it so that we can track changes.
|
||||
lView[directiveDefIdx] = new OnChangesDirectiveWrapper(lView[directiveDefIdx]);
|
||||
}
|
||||
|
||||
const previousOrParentTNode = getPreviousOrParentTNode();
|
||||
postProcessBaseDirective(viewData, previousOrParentTNode, directive, def);
|
||||
postProcessBaseDirective(lView, previousOrParentTNode, directive, def);
|
||||
ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode');
|
||||
if (previousOrParentTNode && previousOrParentTNode.attrs) {
|
||||
setInputsFromAttrs(directiveDefIdx, directive, def.inputs, previousOrParentTNode);
|
||||
setInputsFromAttrs(lView, directiveDefIdx, def, previousOrParentTNode);
|
||||
}
|
||||
|
||||
if (def.contentQueries) {
|
||||
@ -1596,7 +1627,7 @@ function postProcessDirective<T>(
|
||||
}
|
||||
|
||||
if (isComponentDef(def)) {
|
||||
const componentView = getComponentViewByIndex(previousOrParentTNode.index, viewData);
|
||||
const componentView = getComponentViewByIndex(previousOrParentTNode.index, lView);
|
||||
componentView[CONTEXT] = directive;
|
||||
}
|
||||
}
|
||||
@ -1794,20 +1825,53 @@ function addComponentLogic<T>(
|
||||
* @param tNode The static data for this node
|
||||
*/
|
||||
function setInputsFromAttrs<T>(
|
||||
directiveIndex: number, instance: T, inputs: {[P in keyof T]: string;}, tNode: TNode): void {
|
||||
lView: LView, directiveIndex: number, def: DirectiveDef<any>, tNode: TNode): void {
|
||||
let initialInputData = tNode.initialInputs as InitialInputData | undefined;
|
||||
if (initialInputData === undefined || directiveIndex >= initialInputData.length) {
|
||||
initialInputData = generateInitialInputs(directiveIndex, inputs, tNode);
|
||||
initialInputData = generateInitialInputs(directiveIndex, def, tNode);
|
||||
}
|
||||
|
||||
const initialInputs: InitialInputs|null = initialInputData[directiveIndex];
|
||||
if (initialInputs) {
|
||||
for (let i = 0; i < initialInputs.length; i += 2) {
|
||||
(instance as any)[initialInputs[i]] = initialInputs[i + 1];
|
||||
const directiveOrWrappedDirective = lView[directiveIndex];
|
||||
|
||||
for (let i = 0; i < initialInputs.length;) {
|
||||
const privateName = initialInputs[i++];
|
||||
const declaredName = initialInputs[i++];
|
||||
const attrValue = initialInputs[i++];
|
||||
recordChangeAndUpdateProperty(
|
||||
directiveOrWrappedDirective, declaredName, privateName, attrValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the instanced passed as `directiveOrWrappedDirective` is wrapped in {@link
|
||||
* OnChangesDirectiveWrapper} or not.
|
||||
* If it is, it will update the related {@link SimpleChanges} object with the change to signal
|
||||
* `ngOnChanges` hook
|
||||
* should fire, then it will unwrap the instance. After that, it will set the property with the key
|
||||
* provided
|
||||
* in `privateName` on the instance with the passed value.
|
||||
* @param directiveOrWrappedDirective The directive instance or a directive instance wrapped in
|
||||
* {@link OnChangesDirectiveWrapper}
|
||||
* @param declaredName The original, declared name of the property to update.
|
||||
* @param privateName The private, possibly minified name of the property to update.
|
||||
* @param value The value to update the property with.
|
||||
*/
|
||||
function recordChangeAndUpdateProperty<T, K extends keyof T>(
|
||||
directiveOrWrappedDirective: OnChangesDirectiveWrapper<T>| T, declaredName: string,
|
||||
privateName: K, value: any) {
|
||||
let instance: T;
|
||||
if (isOnChangesDirectiveWrapper(directiveOrWrappedDirective)) {
|
||||
instance = unwrapOnChangesDirectiveWrapper(directiveOrWrappedDirective);
|
||||
recordChange(directiveOrWrappedDirective, declaredName, value);
|
||||
} else {
|
||||
instance = directiveOrWrappedDirective;
|
||||
}
|
||||
instance[privateName] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates initialInputData for a node and stores it in the template's static storage
|
||||
* so subsequent template invocations don't have to recalculate it.
|
||||
@ -1824,7 +1888,7 @@ function setInputsFromAttrs<T>(
|
||||
* @param tNode The static data on this node
|
||||
*/
|
||||
function generateInitialInputs(
|
||||
directiveIndex: number, inputs: {[key: string]: string}, tNode: TNode): InitialInputData {
|
||||
directiveIndex: number, directiveDef: DirectiveDef<any>, tNode: TNode): InitialInputData {
|
||||
const initialInputData: InitialInputData = tNode.initialInputs || (tNode.initialInputs = []);
|
||||
initialInputData[directiveIndex] = null;
|
||||
|
||||
@ -1832,19 +1896,23 @@ function generateInitialInputs(
|
||||
let i = 0;
|
||||
while (i < attrs.length) {
|
||||
const attrName = attrs[i];
|
||||
if (attrName === AttributeMarker.SelectOnly) break;
|
||||
// If we hit Select-Only, Classes or Styles, we're done anyway. None of those are valid inputs.
|
||||
if (attrName === AttributeMarker.SelectOnly || attrName === AttributeMarker.Classes ||
|
||||
attrName === AttributeMarker.Styles)
|
||||
break;
|
||||
if (attrName === AttributeMarker.NamespaceURI) {
|
||||
// We do not allow inputs on namespaced attributes.
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
const minifiedInputName = inputs[attrName];
|
||||
const privateName = directiveDef.inputs[attrName];
|
||||
const declaredName = directiveDef.declaredInputs[attrName];
|
||||
const attrValue = attrs[i + 1];
|
||||
|
||||
if (minifiedInputName !== undefined) {
|
||||
if (privateName !== undefined) {
|
||||
const inputsToStore: InitialInputs =
|
||||
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
|
||||
inputsToStore.push(minifiedInputName, attrValue as string);
|
||||
inputsToStore.push(privateName, declaredName, attrValue as string);
|
||||
}
|
||||
|
||||
i += 2;
|
||||
@ -1919,7 +1987,7 @@ export function template(
|
||||
if (currentQueries) {
|
||||
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TContainerNode);
|
||||
}
|
||||
queueLifecycleHooks(tView, tNode);
|
||||
registerPostOrderHooks(tView, tNode);
|
||||
setIsParent(false);
|
||||
}
|
||||
|
||||
@ -2881,7 +2949,7 @@ export function registerContentQuery<Q>(
|
||||
|
||||
export const CLEAN_PROMISE = _CLEAN_PROMISE;
|
||||
|
||||
function initializeTNodeInputs(tNode: TNode | null) {
|
||||
function initializeTNodeInputs(tNode: TNode | null): PropertyAliases|null {
|
||||
// If tNode.inputs is undefined, a listener has created outputs, but inputs haven't
|
||||
// yet been checked.
|
||||
if (tNode) {
|
||||
|
@ -6,8 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ViewEncapsulation} from '../../core';
|
||||
import {SimpleChanges, ViewEncapsulation} from '../../core';
|
||||
import {Type} from '../../interface/type';
|
||||
|
||||
import {CssSelectorList} from './projection';
|
||||
|
||||
|
||||
@ -150,6 +151,7 @@ export interface DirectiveDef<T> extends BaseDef<T> {
|
||||
/* The following are lifecycle hooks for this component */
|
||||
onInit: (() => void)|null;
|
||||
doCheck: (() => void)|null;
|
||||
onChanges: ((changes: SimpleChanges) => void)|null;
|
||||
afterContentInit: (() => void)|null;
|
||||
afterContentChecked: (() => void)|null;
|
||||
afterViewInit: (() => void)|null;
|
||||
|
@ -464,10 +464,12 @@ export type PropertyAliases = {
|
||||
/**
|
||||
* Store the runtime input or output names for all the directives.
|
||||
*
|
||||
* - Even indices: directive index
|
||||
* - Odd indices: minified / internal name
|
||||
* Values are stored in triplets:
|
||||
* - i + 0: directive index
|
||||
* - i + 1: minified / internal name
|
||||
* - i + 2: declared name
|
||||
*
|
||||
* e.g. [0, 'change-minified']
|
||||
* e.g. [0, 'minifiedName', 'declaredPropertyName']
|
||||
*/
|
||||
export type PropertyAliasValue = (number | string)[];
|
||||
|
||||
@ -495,10 +497,12 @@ export type InitialInputData = (InitialInputs | null)[];
|
||||
* Used by InitialInputData to store input properties
|
||||
* that should be set once from attributes.
|
||||
*
|
||||
* Even indices: minified/internal input name
|
||||
* Odd indices: initial value
|
||||
* The inputs come in triplets of:
|
||||
* i + 0: minified/internal input name
|
||||
* i + 1: declared input name (needed for OnChanges)
|
||||
* i + 2: initial value
|
||||
*
|
||||
* e.g. ['role-min', 'button']
|
||||
* e.g. ['minifiedName', 'declaredName', 'value']
|
||||
*/
|
||||
export type InitialInputs = string[];
|
||||
|
||||
|
@ -6,12 +6,12 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {SimpleChanges} from '../../change_detection/simple_change';
|
||||
import {InjectionToken} from '../../di/injection_token';
|
||||
import {Injector} from '../../di/injector';
|
||||
import {Type} from '../../interface/type';
|
||||
import {QueryList} from '../../linker';
|
||||
import {Sanitizer} from '../../sanitization/security';
|
||||
|
||||
import {LContainer} from './container';
|
||||
import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList} from './definition';
|
||||
import {I18nUpdateOpCodes, TI18n} from './i18n';
|
||||
@ -22,7 +22,6 @@ import {RElement, Renderer3, RendererFactory3} from './renderer';
|
||||
import {StylingContext} from './styling';
|
||||
|
||||
|
||||
|
||||
// Below are constants for LView indices to help us look up LView members
|
||||
// without having to remember the specific indices.
|
||||
// Uglify will inline these when minifying so there shouldn't be a cost.
|
||||
@ -533,7 +532,7 @@ export interface RootContext {
|
||||
* Even indices: Directive index
|
||||
* Odd indices: Hook function
|
||||
*/
|
||||
export type HookData = (number | (() => void))[];
|
||||
export type HookData = (number | (() => void) | ((changes: SimpleChanges) => void))[];
|
||||
|
||||
/**
|
||||
* Static data that corresponds to the instance-specific data array on an LView.
|
||||
|
@ -115,7 +115,6 @@ export interface R3DirectiveMetadataFacade {
|
||||
queries: R3QueryMetadataFacade[];
|
||||
host: {[key: string]: string};
|
||||
propMetadata: {[key: string]: any[]};
|
||||
lifecycle: {usesOnChanges: boolean;};
|
||||
inputs: string[];
|
||||
outputs: string[];
|
||||
usesInheritance: boolean;
|
||||
|
@ -145,9 +145,6 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
|
||||
inputs: metadata.inputs || EMPTY_ARRAY,
|
||||
outputs: metadata.outputs || EMPTY_ARRAY,
|
||||
queries: extractQueriesMetadata(type, propMetadata, isContentQuery),
|
||||
lifecycle: {
|
||||
usesOnChanges: type.prototype.ngOnChanges !== undefined,
|
||||
},
|
||||
typeSourceSpan: null !,
|
||||
usesInheritance: !extendsDirectlyFromObject(type),
|
||||
exportAs: extractExportAs(metadata.exportAs),
|
||||
|
@ -31,7 +31,6 @@ export const angularCoreEnv: {[name: string]: Function} = {
|
||||
'inject': inject,
|
||||
'ɵinjectAttribute': r3.injectAttribute,
|
||||
'ɵtemplateRefExtractor': r3.templateRefExtractor,
|
||||
'ɵNgOnChangesFeature': r3.NgOnChangesFeature,
|
||||
'ɵProvidersFeature': r3.ProvidersFeature,
|
||||
'ɵInheritDefinitionFeature': r3.InheritDefinitionFeature,
|
||||
'ɵelementAttribute': r3.elementAttribute,
|
||||
|
71
packages/core/src/render3/onchanges_util.ts
Normal file
71
packages/core/src/render3/onchanges_util.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {SimpleChange, SimpleChanges} from '../change_detection/simple_change';
|
||||
|
||||
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
* Checks an object to see if it's an exact instance of a particular type
|
||||
* without traversing the inheritance hierarchy like `instanceof` does.
|
||||
* @param obj The object to check
|
||||
* @param type The type to check the object against
|
||||
*/
|
||||
export function isExactInstanceOf<T>(obj: any, type: Constructor<T>): obj is T {
|
||||
return obj != null && typeof obj == 'object' && Object.getPrototypeOf(obj) == type.prototype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if an object is an instance of {@link OnChangesDirectiveWrapper}
|
||||
* @param obj the object to check (generally from `LView`)
|
||||
*/
|
||||
export function isOnChangesDirectiveWrapper(obj: any): obj is OnChangesDirectiveWrapper<any> {
|
||||
return isExactInstanceOf(obj, OnChangesDirectiveWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the `OnChangesDirectiveWrapper` if present.
|
||||
*
|
||||
* @param obj to unwrap.
|
||||
*/
|
||||
export function unwrapOnChangesDirectiveWrapper<T>(obj: T | OnChangesDirectiveWrapper<T>): T {
|
||||
return isOnChangesDirectiveWrapper(obj) ? obj.instance : obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that wraps directive instances for storage in LView when directives
|
||||
* have onChanges hooks to deal with.
|
||||
*/
|
||||
export class OnChangesDirectiveWrapper<T = any> {
|
||||
seenProps = new Set<string>();
|
||||
previous: SimpleChanges = {};
|
||||
changes: SimpleChanges|null = null;
|
||||
|
||||
constructor(public instance: T) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the `changes` property on the `wrapper` instance, such that when it's
|
||||
* checked in {@link callHooks} it will fire the related `onChanges` hook.
|
||||
* @param wrapper the wrapper for the directive instance
|
||||
* @param declaredName the declared name to be used in `SimpleChange`
|
||||
* @param value The new value for the property
|
||||
*/
|
||||
export function recordChange(wrapper: OnChangesDirectiveWrapper, declaredName: string, value: any) {
|
||||
const simpleChanges = wrapper.changes || (wrapper.changes = {});
|
||||
|
||||
const firstChange = !wrapper.seenProps.has(declaredName);
|
||||
if (firstChange) {
|
||||
wrapper.seenProps.add(declaredName);
|
||||
}
|
||||
|
||||
const previous = wrapper.previous;
|
||||
const previousValue: SimpleChange|undefined = previous[declaredName];
|
||||
simpleChanges[declaredName] = new SimpleChange(
|
||||
firstChange ? undefined : previousValue && previousValue.currentValue, value, firstChange);
|
||||
}
|
@ -17,6 +17,7 @@ import {TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './inte
|
||||
import {GlobalTargetName, GlobalTargetResolver, RComment, RElement, RText} from './interfaces/renderer';
|
||||
import {StylingContext} from './interfaces/styling';
|
||||
import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view';
|
||||
import {isOnChangesDirectiveWrapper} from './onchanges_util';
|
||||
|
||||
|
||||
/**
|
||||
@ -67,9 +68,14 @@ export function flatten(list: any[]): any[] {
|
||||
/** Retrieves a value from any `LView` or `TData`. */
|
||||
export function loadInternal<T>(view: LView | TData, index: number): T {
|
||||
ngDevMode && assertDataInRange(view, index + HEADER_OFFSET);
|
||||
return view[index + HEADER_OFFSET];
|
||||
const record = view[index + HEADER_OFFSET];
|
||||
// If we're storing an array because of a directive or component with ngOnChanges,
|
||||
// return the directive or component instance.
|
||||
return isOnChangesDirectiveWrapper(record) ? record.instance : record;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Takes the value of a slot in `LView` and returns the element node.
|
||||
*
|
||||
@ -288,4 +294,4 @@ export function resolveDocument(element: RElement & {ownerDocument: Document}) {
|
||||
|
||||
export function resolveBody(element: RElement & {ownerDocument: Document}) {
|
||||
return {name: 'body', target: element.ownerDocument.body};
|
||||
}
|
||||
}
|
||||
|
@ -11,10 +11,11 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec
|
||||
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
|
||||
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
|
||||
|
||||
import {checkNoChanges, checkNoChangesInRootView, checkView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions';
|
||||
import {checkNoChanges, checkNoChangesInRootView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions';
|
||||
import {TNode, TNodeType, TViewNode} from './interfaces/node';
|
||||
import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT, RENDERER_FACTORY} from './interfaces/view';
|
||||
import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT} from './interfaces/view';
|
||||
import {destroyLView} from './node_manipulation';
|
||||
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
|
||||
import {getNativeByTNode} from './util';
|
||||
|
||||
|
||||
@ -271,7 +272,8 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
|
||||
}
|
||||
|
||||
private _lookUpContext(): T {
|
||||
return this._context = this._lView[PARENT] ![this._componentIndex] as T;
|
||||
return this._context =
|
||||
unwrapOnChangesDirectiveWrapper(this._lView[PARENT] ![this._componentIndex] as T);
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user