feat(ivy): Add InheritanceDefinitionFeature to support directive inheritance (#24570)
- Adds InheritanceDefinitionFeature to ivy - Ensures that lifecycle hooks are inherited from super classes whether they are defined as directives or not - Directives cannot inherit from Components - Components can inherit from Directives or Components - Ensures that Inputs, Outputs, and Host Bindings are inherited - Ensures that super class Features are run PR Close #24570
This commit is contained in:
120
packages/core/src/render3/features/inherit_definition_feature.ts
Normal file
120
packages/core/src/render3/features/inherit_definition_feature.ts
Normal file
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @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 {Type} from '../../type';
|
||||
import {ComponentDefInternal, ComponentType, DirectiveDefFeature, DirectiveDefInternal} from '../interfaces/definition';
|
||||
|
||||
|
||||
/**
|
||||
* Sets properties on a target object from a source object, but only if
|
||||
* the property doesn't already exist on the target object.
|
||||
* @param target The target to set properties on
|
||||
* @param source The source of the property keys and values to set
|
||||
*/
|
||||
function fillProperties(target: {[key: string]: string}, source: {[key: string]: string}) {
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key) && !target.hasOwnProperty(key)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Determines if a definition is a {@link ComponentDefInternal} or a {@link DirectiveDefInternal}
|
||||
* @param definition The definition to examine
|
||||
*/
|
||||
function isComponentDef<T>(definition: ComponentDefInternal<T>| DirectiveDefInternal<T>):
|
||||
definition is ComponentDefInternal<T> {
|
||||
const def = definition as ComponentDefInternal<T>;
|
||||
return typeof def.template === 'function';
|
||||
}
|
||||
|
||||
function getSuperType(type: Type<any>): Type<any>&
|
||||
{ngComponentDef?: ComponentDefInternal<any>, ngDirectiveDef?: DirectiveDefInternal<any>} {
|
||||
return Object.getPrototypeOf(type.prototype).constructor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the definition from a super class to a sub class.
|
||||
* @param definition The definition that is a SubClass of another directive of component
|
||||
*/
|
||||
export function InheritDefinitionFeature(
|
||||
definition: DirectiveDefInternal<any>| ComponentDefInternal<any>): void {
|
||||
let superType = getSuperType(definition.type);
|
||||
let superDef: DirectiveDefInternal<any>|ComponentDefInternal<any>|undefined = undefined;
|
||||
|
||||
while (superType && !superDef) {
|
||||
if (isComponentDef(definition)) {
|
||||
superDef = superType.ngComponentDef || superType.ngDirectiveDef;
|
||||
} else {
|
||||
if (superType.ngComponentDef) {
|
||||
throw new Error('Directives cannot inherit Components');
|
||||
}
|
||||
superDef = superType.ngDirectiveDef;
|
||||
}
|
||||
|
||||
if (superDef) {
|
||||
// Merge inputs and outputs
|
||||
fillProperties(definition.inputs, superDef.inputs);
|
||||
fillProperties(definition.declaredInputs, superDef.declaredInputs);
|
||||
fillProperties(definition.outputs, superDef.outputs);
|
||||
|
||||
// Merge hostBindings
|
||||
const prevHostBindings = definition.hostBindings;
|
||||
const superHostBindings = superDef.hostBindings;
|
||||
if (superHostBindings) {
|
||||
if (prevHostBindings) {
|
||||
definition.hostBindings = (directiveIndex: number, elementIndex: number) => {
|
||||
superHostBindings(directiveIndex, elementIndex);
|
||||
prevHostBindings(directiveIndex, elementIndex);
|
||||
};
|
||||
} else {
|
||||
definition.hostBindings = superHostBindings;
|
||||
}
|
||||
}
|
||||
|
||||
// Inherit hooks
|
||||
// Assume super class inheritance feature has already run.
|
||||
definition.afterContentChecked =
|
||||
definition.afterContentChecked || superDef.afterContentChecked;
|
||||
definition.afterContentInit = definition.afterContentInit || superDef.afterContentInit;
|
||||
definition.afterViewChecked = definition.afterViewChecked || superDef.afterViewChecked;
|
||||
definition.afterViewInit = definition.afterViewInit || superDef.afterViewInit;
|
||||
definition.doCheck = definition.doCheck || superDef.doCheck;
|
||||
definition.onDestroy = definition.onDestroy || superDef.onDestroy;
|
||||
definition.onInit = definition.onInit || superDef.onInit;
|
||||
|
||||
// Run parent features
|
||||
const features = superDef.features;
|
||||
if (features) {
|
||||
for (const feature of features) {
|
||||
if (feature && feature !== InheritDefinitionFeature) {
|
||||
(feature as DirectiveDefFeature)(definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Even if we don't have a definition, check the type for the hooks and use those if need be
|
||||
const superPrototype = superType.prototype;
|
||||
|
||||
if (superPrototype) {
|
||||
definition.afterContentChecked =
|
||||
definition.afterContentChecked || superPrototype.afterContentChecked;
|
||||
definition.afterContentInit =
|
||||
definition.afterContentInit || superPrototype.afterContentInit;
|
||||
definition.afterViewChecked =
|
||||
definition.afterViewChecked || superPrototype.afterViewChecked;
|
||||
definition.afterViewInit = definition.afterViewInit || superPrototype.afterViewInit;
|
||||
definition.doCheck = definition.doCheck || superPrototype.doCheck;
|
||||
definition.onDestroy = definition.onDestroy || superPrototype.onDestroy;
|
||||
definition.onInit = definition.onInit || superPrototype.onInit;
|
||||
}
|
||||
}
|
||||
|
||||
superType = Object.getPrototypeOf(superType);
|
||||
}
|
||||
}
|
116
packages/core/src/render3/features/ng_onchanges_feature.ts
Normal file
116
packages/core/src/render3/features/ng_onchanges_feature.ts
Normal file
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @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 {DirectiveDefInternal} 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: DirectiveDefInternal<T>): void {
|
||||
const declaredToMinifiedInputs = definition.declaredInputs;
|
||||
const proto = definition.type.prototype;
|
||||
for (const declaredName in declaredToMinifiedInputs) {
|
||||
if (declaredToMinifiedInputs.hasOwnProperty(declaredName)) {
|
||||
const minifiedKey = declaredToMinifiedInputs[declaredName];
|
||||
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[declaredName];
|
||||
|
||||
if (currentChange) {
|
||||
currentChange.currentValue = value;
|
||||
} else {
|
||||
simpleChanges[declaredName] =
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
19
packages/core/src/render3/features/public_feature.ts
Normal file
19
packages/core/src/render3/features/public_feature.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @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 {diPublic} from '../di';
|
||||
import {DirectiveDefInternal} from '../interfaces/definition';
|
||||
|
||||
/**
|
||||
* This feature publishes the directive (or component) into the DI system, making it visible to
|
||||
* others for injection.
|
||||
*
|
||||
* @param definition
|
||||
*/
|
||||
export function PublicFeature<T>(definition: DirectiveDefInternal<T>) {
|
||||
definition.diPublic = diPublic;
|
||||
}
|
Reference in New Issue
Block a user