refactor(ivy): Add newer, smaller NgOnChangesFeature (#28187)
PR Close #28187
This commit is contained in:

committed by
Alex Rickabaugh

parent
5552661fd7
commit
a95e81978b
@ -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) ||
|
||||
|
@ -5,19 +5,9 @@
|
||||
* 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, SimpleChange} from './simple_change';
|
||||
import {SimpleChanges} from './simple_change';
|
||||
|
||||
|
||||
/**
|
||||
* Defines an object that associates properties with
|
||||
* instances of `SimpleChange`.
|
||||
*
|
||||
* @see `OnChanges`
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface SimpleChanges { [propName: string]: SimpleChange; }
|
||||
|
||||
/**
|
||||
* @description
|
||||
* A lifecycle hook that is called when any data-bound property of a directive changes.
|
||||
|
@ -17,6 +17,7 @@ import {assertComponentType} from './assert';
|
||||
import {getComponentDef} from './definition';
|
||||
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
|
||||
import {publishDefaultGlobalUtils} from './global_utils';
|
||||
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';
|
||||
@ -25,7 +26,6 @@ import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './inte
|
||||
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view';
|
||||
import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setCurrentDirectiveDef} from './state';
|
||||
import {defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util';
|
||||
import { registerPreOrderHooks, registerPostOrderHooks } from './hooks';
|
||||
|
||||
|
||||
|
||||
@ -240,7 +240,8 @@ export function LifecycleHooksFeature(component: any, def: ComponentDef<any>): v
|
||||
registerPreOrderHooks(dirIndex, def, rootTView);
|
||||
// TODO(misko): replace `as TNode` with createTNode call. (needs refactoring to lose dep on
|
||||
// LNode).
|
||||
registerPostOrderHooks(rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
|
||||
registerPostOrderHooks(
|
||||
rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -276,6 +276,7 @@ export function defineComponent<T>(componentDefinition: {
|
||||
id: 'c',
|
||||
styles: componentDefinition.styles || EMPTY_ARRAY,
|
||||
_: null as never,
|
||||
setInput: null,
|
||||
};
|
||||
def._ = noSideEffects(() => {
|
||||
const directiveTypes = componentDefinition.directives !;
|
||||
@ -380,12 +381,14 @@ export function defineNgModule<T>(def: {type: T} & Partial<NgModuleDef<T>>): nev
|
||||
*
|
||||
|
||||
*/
|
||||
function invertObject(obj: any, secondary?: any): any {
|
||||
if (obj == null) return EMPTY_OBJ;
|
||||
function invertObject<T>(
|
||||
obj?: {[P in keyof T]?: string | [string, string]},
|
||||
secondary?: {[key: string]: string}): {[P in keyof T]: string} {
|
||||
if (obj == null) return EMPTY_OBJ as any;
|
||||
const newLookup: any = {};
|
||||
for (const minifiedKey in obj) {
|
||||
if (obj.hasOwnProperty(minifiedKey)) {
|
||||
let publicName: string = obj[minifiedKey];
|
||||
let publicName: string|[string, string] = obj[minifiedKey] !;
|
||||
let declaredName = publicName;
|
||||
if (Array.isArray(publicName)) {
|
||||
declaredName = publicName[1];
|
||||
@ -393,7 +396,7 @@ function invertObject(obj: any, secondary?: any): any {
|
||||
}
|
||||
newLookup[publicName] = minifiedKey;
|
||||
if (secondary) {
|
||||
(secondary[publicName] = declaredName);
|
||||
(secondary[publicName] = declaredName as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -470,11 +473,11 @@ export function defineBase<T>(baseDefinition: {
|
||||
*/
|
||||
outputs?: {[P in keyof T]?: string};
|
||||
}): BaseDef<T> {
|
||||
const declaredInputs: {[P in keyof T]: P} = {} as any;
|
||||
const declaredInputs: {[P in keyof T]: string} = {} as any;
|
||||
return {
|
||||
inputs: invertObject(baseDefinition.inputs, declaredInputs),
|
||||
inputs: invertObject<T>(baseDefinition.inputs as any, declaredInputs),
|
||||
declaredInputs: declaredInputs,
|
||||
outputs: invertObject(baseDefinition.outputs),
|
||||
outputs: invertObject<T>(baseDefinition.outputs as any),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -7,10 +7,10 @@
|
||||
*/
|
||||
|
||||
import {Type} from '../../interface/type';
|
||||
import {Component} from '../../metadata/directives';
|
||||
import {fillProperties} from '../../util/property';
|
||||
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
|
||||
import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition';
|
||||
import { Component } from '../../metadata/directives';
|
||||
|
||||
|
||||
|
||||
|
@ -6,9 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {SimpleChange} from '../../change_detection/change_detection_util';
|
||||
import {SimpleChanges} from '../../interface/simple_change';
|
||||
import {OnChanges} from '../../interface/lifecycle_hooks';
|
||||
import {SimpleChange, SimpleChanges} from '../../interface/simple_change';
|
||||
import {EMPTY_OBJ} from '../empty';
|
||||
import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition';
|
||||
|
||||
const PRIVATE_PREFIX = '__ngOnChanges_';
|
||||
@ -40,86 +40,63 @@ type OnChangesExpando = OnChanges & {
|
||||
* ```
|
||||
*/
|
||||
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;
|
||||
if (definition.type.prototype.ngOnChanges) {
|
||||
definition.setInput = ngOnChangesSetInput;
|
||||
|
||||
// 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 prevDoCheck = definition.doCheck;
|
||||
const prevOnInit = definition.onInit;
|
||||
|
||||
const getter = originalProperty && originalProperty.get;
|
||||
const setter = originalProperty && originalProperty.set;
|
||||
definition.onInit = wrapOnChanges(prevOnInit);
|
||||
definition.doCheck = wrapOnChanges(prevDoCheck);
|
||||
}
|
||||
}
|
||||
|
||||
// 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});
|
||||
}
|
||||
function wrapOnChanges(hook: (() => void) | null) {
|
||||
return function(this: OnChanges) {
|
||||
const simpleChangesStore = getSimpleChangesStore(this);
|
||||
const current = simpleChangesStore && simpleChangesStore.current;
|
||||
|
||||
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 (current) {
|
||||
simpleChangesStore !.previous = current;
|
||||
simpleChangesStore !.current = null;
|
||||
this.ngOnChanges(current);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
hook && hook.call(this);
|
||||
};
|
||||
}
|
||||
|
||||
definition.doCheck = onChangesWrapper(definition.doCheck);
|
||||
function ngOnChangesSetInput<T>(
|
||||
this: DirectiveDef<T>, instance: T, value: any, publicName: string, privateName: string): void {
|
||||
const simpleChangesStore = getSimpleChangesStore(instance) ||
|
||||
setSimpleChangesStore(instance, {previous: EMPTY_OBJ, current: null});
|
||||
const current = simpleChangesStore.current || (simpleChangesStore.current = {});
|
||||
const previous = simpleChangesStore.previous;
|
||||
|
||||
const declaredName = (this.declaredInputs as{[key: string]: string})[publicName];
|
||||
const previousChange = previous[declaredName];
|
||||
current[declaredName] = new SimpleChange(
|
||||
previousChange && previousChange.currentValue, value, previous === EMPTY_OBJ);
|
||||
|
||||
(instance as any)[privateName] = value;
|
||||
}
|
||||
|
||||
const SIMPLE_CHANGES_STORE = '__ngSimpleChanges__';
|
||||
|
||||
function getSimpleChangesStore(instance: any): null|NgSimpleChangesStore {
|
||||
return instance[SIMPLE_CHANGES_STORE] || null;
|
||||
}
|
||||
|
||||
function setSimpleChangesStore(instance: any, store: NgSimpleChangesStore): NgSimpleChangesStore {
|
||||
return instance[SIMPLE_CHANGES_STORE] = store;
|
||||
}
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
interface NgSimpleChangesStore {
|
||||
previous: SimpleChanges;
|
||||
current: SimpleChanges|null;
|
||||
}
|
||||
|
@ -959,10 +959,10 @@ function listenerInternal(
|
||||
const propsLength = props.length;
|
||||
if (propsLength) {
|
||||
const lCleanup = getCleanup(lView);
|
||||
for (let i = 0; i < propsLength; i += 2) {
|
||||
for (let i = 0; i < propsLength; i += 3) {
|
||||
const index = props[i] as number;
|
||||
ngDevMode && assertDataInRange(lView, index);
|
||||
const minifiedName = props[i + 1];
|
||||
const minifiedName = props[i + 2];
|
||||
const directiveInstance = lView[index];
|
||||
const output = directiveInstance[minifiedName];
|
||||
|
||||
@ -1214,18 +1214,29 @@ export function createTNode(
|
||||
* @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;
|
||||
const tView = lView[TVIEW];
|
||||
for (let i = 0; i < inputs.length;) {
|
||||
const index = inputs[i++] as number;
|
||||
const publicName = inputs[i++] as string;
|
||||
const privateName = inputs[i++] as string;
|
||||
const instance = lView[index];
|
||||
ngDevMode && assertDataInRange(lView, index);
|
||||
const def = tView.data[index] as DirectiveDef<any>;
|
||||
const setInput = def.setInput;
|
||||
if (setInput) {
|
||||
def.setInput !(instance, value, publicName, privateName);
|
||||
} else {
|
||||
instance[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; i += 3) {
|
||||
const renderer = lView[RENDERER];
|
||||
const attrName = normalizeDebugBindingName(inputs[i + 1] as string);
|
||||
const attrName = normalizeDebugBindingName(inputs[i + 2] as string);
|
||||
const debugValue = normalizeDebugBindingValue(value);
|
||||
if (type === TNodeType.Element) {
|
||||
isProceduralRenderer(renderer) ?
|
||||
@ -1268,8 +1279,8 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro
|
||||
propStore = propStore || {};
|
||||
const internalName = propertyAliasMap[publicName];
|
||||
const hasProperty = propStore.hasOwnProperty(publicName);
|
||||
hasProperty ? propStore[publicName].push(i, internalName) :
|
||||
(propStore[publicName] = [i, internalName]);
|
||||
hasProperty ? propStore[publicName].push(i, publicName, internalName) :
|
||||
(propStore[publicName] = [i, publicName, internalName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1702,7 +1713,7 @@ function postProcessDirective<T>(
|
||||
postProcessBaseDirective(viewData, previousOrParentTNode, directive, def);
|
||||
ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode');
|
||||
if (previousOrParentTNode && previousOrParentTNode.attrs) {
|
||||
setInputsFromAttrs(directiveDefIdx, directive, def.inputs, previousOrParentTNode);
|
||||
setInputsFromAttrs(directiveDefIdx, directive, def, previousOrParentTNode);
|
||||
}
|
||||
|
||||
if (def.contentQueries) {
|
||||
@ -1903,16 +1914,24 @@ 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 {
|
||||
directiveIndex: number, instance: T, def: DirectiveDef<T>, 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.inputs, 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 setInput = def.setInput;
|
||||
for (let i = 0; i < initialInputs.length;) {
|
||||
const publicName = initialInputs[i++];
|
||||
const privateName = initialInputs[i++];
|
||||
const value = initialInputs[i++];
|
||||
if (setInput) {
|
||||
def.setInput !(instance, value, publicName, privateName);
|
||||
} else {
|
||||
(instance as any)[privateName] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1956,7 +1975,7 @@ function generateInitialInputs(
|
||||
if (minifiedInputName !== undefined) {
|
||||
const inputsToStore: InitialInputs =
|
||||
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
|
||||
inputsToStore.push(minifiedInputName, attrValue as string);
|
||||
inputsToStore.push(attrName, minifiedInputName, attrValue as string);
|
||||
}
|
||||
|
||||
i += 2;
|
||||
|
@ -84,14 +84,14 @@ export interface BaseDef<T> {
|
||||
* @deprecated This is only here because `NgOnChanges` incorrectly uses declared name instead of
|
||||
* public or minified name.
|
||||
*/
|
||||
readonly declaredInputs: {[P in keyof T]: P};
|
||||
readonly declaredInputs: {[P in keyof T]: string};
|
||||
|
||||
/**
|
||||
* A dictionary mapping the outputs' minified property names to their public API names, which
|
||||
* are their aliases if any, or their original unminified property names
|
||||
* (as in `@Output('alias') propertyName: any;`).
|
||||
*/
|
||||
readonly outputs: {[P in keyof T]: P};
|
||||
readonly outputs: {[P in keyof T]: string};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,6 +152,10 @@ export interface DirectiveDef<T> extends BaseDef<T> {
|
||||
* The features applied to this directive
|
||||
*/
|
||||
readonly features: DirectiveDefFeature[]|null;
|
||||
|
||||
setInput:
|
||||
((this: DirectiveDef<T>, instance: T, value: any, publicName: string,
|
||||
privateName: string) => void)|null;
|
||||
}
|
||||
|
||||
export type ComponentDefWithMeta<
|
||||
|
@ -468,10 +468,11 @@ export type PropertyAliases = {
|
||||
/**
|
||||
* Store the runtime input or output names for all the directives.
|
||||
*
|
||||
* - Even indices: directive index
|
||||
* - Odd indices: minified / internal name
|
||||
* i+0: directive instance index
|
||||
* i+1: publicName
|
||||
* i+2: privateName
|
||||
*
|
||||
* e.g. [0, 'change-minified']
|
||||
* e.g. [0, 'change', 'change-minified']
|
||||
*/
|
||||
export type PropertyAliasValue = (number | string)[];
|
||||
|
||||
@ -484,14 +485,15 @@ export type PropertyAliasValue = (number | string)[];
|
||||
*
|
||||
* Within each sub-array:
|
||||
*
|
||||
* Even indices: minified/internal input name
|
||||
* Odd indices: initial value
|
||||
* i+0: attribute name
|
||||
* i+1: minified/internal input name
|
||||
* i+2: initial value
|
||||
*
|
||||
* If a directive on a node does not have any input properties
|
||||
* that should be set from attributes, its index is set to null
|
||||
* to avoid a sparse array.
|
||||
*
|
||||
* e.g. [null, ['role-min', 'button']]
|
||||
* e.g. [null, ['role-min', 'minified-input', 'button']]
|
||||
*/
|
||||
export type InitialInputData = (InitialInputs | null)[];
|
||||
|
||||
@ -499,10 +501,11 @@ 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
|
||||
* i+0: attribute name
|
||||
* i+1: minified/internal input name
|
||||
* i+2: initial value
|
||||
*
|
||||
* e.g. ['role-min', 'button']
|
||||
* e.g. ['role-min', 'minified-input', 'button']
|
||||
*/
|
||||
export type InitialInputs = string[];
|
||||
|
||||
|
@ -8,7 +8,6 @@
|
||||
|
||||
import {InjectionToken} from '../../di/injection_token';
|
||||
import {Injector} from '../../di/injector';
|
||||
import {SimpleChanges} from '../../interface/simple_change';
|
||||
import {Type} from '../../interface/type';
|
||||
import {QueryList} from '../../linker';
|
||||
import {Sanitizer} from '../../sanitization/security';
|
||||
|
Reference in New Issue
Block a user