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:
Ben Lesh
2018-06-18 08:05:06 -07:00
committed by Misko Hevery
parent 13d60eac61
commit 9803cb011e
25 changed files with 1095 additions and 292 deletions

View File

@ -61,14 +61,47 @@ export function defineComponent<T>(componentDefinition: {
/**
* A map of input names.
*
* The format is in: `{[actualPropertyName: string]:string}`.
* The format is in: `{[actualPropertyName: string]:(string|[string, string])}`.
*
* Which the minifier may translate to: `{[minifiedPropertyName: string]:string}`.
* Given:
* ```
* class MyComponent {
* @Input()
* publicInput1: string;
*
* This allows the render to re-construct the minified and non-minified names
* @Input('publicInput2')
* declaredInput2: string;
* }
* ```
*
* is described as:
* ```
* {
* publicInput1: 'publicInput1',
* declaredInput2: ['declaredInput2', 'publicInput2'],
* }
* ```
*
* Which the minifier may translate to:
* ```
* {
* minifiedPublicInput1: 'publicInput1',
* minifiedDeclaredInput2: [ 'publicInput2', 'declaredInput2'],
* }
* ```
*
* This allows the render to re-construct the minified, public, and declared names
* of properties.
*
* NOTE:
* - Because declared and public name are usually same we only generate the array
* `['declared', 'public']` format when they differ.
* - The reason why this API and `outputs` API is not the same is that `NgOnChanges` has
* inconsistent behavior in that it uses declared names rather than minified or public. For
* this reason `NgOnChanges` will be deprecated and removed in future version and this
* API will be simplified to be consistent with `output`.
*/
inputs?: {[P in keyof T]?: string};
inputs?: {[P in keyof T]?: string | [string, string]};
/**
* A map of output names.
@ -176,6 +209,7 @@ export function defineComponent<T>(componentDefinition: {
const type = componentDefinition.type;
const pipeTypes = componentDefinition.pipes !;
const directiveTypes = componentDefinition.directives !;
const declaredInputs: {[P in keyof T]: P} = {} as any;
const def: ComponentDefInternal<any> = {
type: type,
diPublic: null,
@ -183,7 +217,8 @@ export function defineComponent<T>(componentDefinition: {
template: componentDefinition.template || null !,
hostBindings: componentDefinition.hostBindings || null,
attributes: componentDefinition.attributes || null,
inputs: invertObject(componentDefinition.inputs),
inputs: invertObject(componentDefinition.inputs, declaredInputs),
declaredInputs: declaredInputs,
outputs: invertObject(componentDefinition.outputs),
rendererType: resolveRendererType2(componentDefinition.rendererType) || null,
exportAs: componentDefinition.exportAs || null,
@ -204,6 +239,7 @@ export function defineComponent<T>(componentDefinition: {
null,
selectors: componentDefinition.selectors,
viewQuery: componentDefinition.viewQuery || null,
features: componentDefinition.features || null,
};
const feature = componentDefinition.features;
feature && feature.forEach((fn) => fn(def));
@ -239,115 +275,72 @@ export function defineNgModule<T>(def: {type: T} & Partial<NgModuleDef<T, any, a
return res as never;
}
const PRIVATE_PREFIX = '__ngOnChanges_';
type OnChangesExpando = OnChanges & {
__ngOnChanges_: SimpleChanges|null|undefined;
[key: string]: any;
};
/**
* Creates an NgOnChangesFeature function for a component's features list.
*
* It accepts an optional map of minified input property names to original property names,
* if any input properties have a public alias.
*
* The NgOnChangesFeature function that is returned decorates a component with support for
* the ngOnChanges lifecycle hook, so it should be included in any component that implements
* that hook.
*
* Example usage:
*
* ```
* static ngComponentDef = defineComponent({
* ...
* inputs: {name: 'publicName'},
* features: [NgOnChangesFeature({name: 'name'})]
* });
* ```
*
* @param inputPropertyNames Map of input property names, if they are aliased
* @returns DirectiveDefFeature
*/
export function NgOnChangesFeature(inputPropertyNames?: {[key: string]: string}):
DirectiveDefFeature {
return function(definition: DirectiveDefInternal<any>): void {
const inputs = definition.inputs;
const proto = definition.type.prototype;
for (let pubKey in inputs) {
const minKey = inputs[pubKey];
const propertyName = inputPropertyNames && inputPropertyNames[minKey] || pubKey;
const privateMinKey = PRIVATE_PREFIX + minKey;
const originalProperty = Object.getOwnPropertyDescriptor(proto, minKey);
const getter = originalProperty && originalProperty.get;
const setter = originalProperty && originalProperty.set;
// create a getter and setter for property
Object.defineProperty(proto, minKey, {
get: getter ||
(setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }),
set: function(this: OnChangesExpando, value: any) {
let simpleChanges = this[PRIVATE_PREFIX];
if (!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: SimpleChange|undefined = simpleChanges[propertyName];
if (currentChange) {
currentChange.currentValue = value;
} else {
simpleChanges[propertyName] =
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;
}
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) {
let simpleChanges = this[PRIVATE_PREFIX];
if (simpleChanges != null) {
this.ngOnChanges(simpleChanges);
this[PRIVATE_PREFIX] = null;
}
delegateHook && delegateHook.apply(this);
};
}
}
export function PublicFeature<T>(definition: DirectiveDefInternal<T>) {
definition.diPublic = diPublic;
}
const EMPTY = {};
/** Swaps the keys and values of an object. */
function invertObject(obj: any): any {
/**
* Inverts an inputs or outputs lookup such that the keys, which were the
* minified keys, are part of the values, and the values are parsed so that
* the publicName of the property is the new key
*
* e.g. for
*
* ```
* class Comp {
* @Input()
* propName1: string;
*
* @Input('publicName')
* propName2: number;
* }
* ```
*
* will be serialized as
*
* ```
* {
* a0: 'propName1',
* b1: ['publicName', 'propName2'],
* }
* ```
*
* becomes
*
* ```
* {
* 'propName1': 'a0',
* 'publicName': 'b1'
* }
* ```
*
* Optionally the function can take `secondary` which will result in:
*
* ```
* {
* 'propName1': 'a0',
* 'propName2': 'b1'
* }
* ```
*
*/
function invertObject(obj: any, secondary?: any): any {
if (obj == null) return EMPTY;
const newObj: any = {};
for (let minifiedKey in obj) {
newObj[obj[minifiedKey]] = minifiedKey;
const newLookup: any = {};
for (const minifiedKey in obj) {
if (obj.hasOwnProperty(minifiedKey)) {
let publicName = obj[minifiedKey];
let declaredName = publicName;
if (Array.isArray(publicName)) {
declaredName = publicName[1];
publicName = publicName[0];
}
newLookup[publicName] = minifiedKey;
if (secondary) {
(secondary[declaredName] = minifiedKey);
}
}
}
return newObj;
return newLookup;
}
/**
@ -389,14 +382,47 @@ export const defineDirective = defineComponent as any as<T>(directiveDefinition:
/**
* A map of input names.
*
* The format is in: `{[actualPropertyName: string]:string}`.
* The format is in: `{[actualPropertyName: string]:(string|[string, string])}`.
*
* Which the minifier may translate to: `{[minifiedPropertyName: string]:string}`.
* Given:
* ```
* class MyComponent {
* @Input()
* publicInput1: string;
*
* This allows the render to re-construct the minified and non-minified names
* @Input('publicInput2')
* declaredInput2: string;
* }
* ```
*
* is described as:
* ```
* {
* publicInput1: 'publicInput1',
* declaredInput2: ['declaredInput2', 'publicInput2'],
* }
* ```
*
* Which the minifier may translate to:
* ```
* {
* minifiedPublicInput1: 'publicInput1',
* minifiedDeclaredInput2: [ 'publicInput2', 'declaredInput2'],
* }
* ```
*
* This allows the render to re-construct the minified, public, and declared names
* of properties.
*
* NOTE:
* - Because declared and public name are usually same we only generate the array
* `['declared', 'public']` format when they differ.
* - The reason why this API and `outputs` API is not the same is that `NgOnChanges` has
* inconsistent behavior in that it uses declared names rather than minified or public. For
* this reason `NgOnChanges` will be deprecated and removed in future version and this
* API will be simplified to be consistent with `output`.
*/
inputs?: {[P in keyof T]?: string};
inputs?: {[P in keyof T]?: string | [string, string]};
/**
* A map of output names.
@ -413,7 +439,7 @@ export const defineDirective = defineComponent as any as<T>(directiveDefinition:
/**
* A list of optional features to apply.
*
* See: {@link NgOnChangesFeature}, {@link PublicFeature}
* See: {@link NgOnChangesFeature}, {@link PublicFeature}, {@link InheritDefinitionFeature}
*/
features?: DirectiveDefFeature[];

View 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);
}
}

View 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);
};
}

View 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;
}

View File

@ -7,9 +7,13 @@
*/
import {LifecycleHooksFeature, getHostElement, getRenderedText, renderComponent, whenRendered} from './component';
import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, defineNgModule, definePipe} from './definition';
import {defineComponent, defineDirective, defineNgModule, definePipe} from './definition';
import {InheritDefinitionFeature} from './features/inherit_definition_feature';
import {NgOnChangesFeature} from './features/ng_onchanges_feature';
import {PublicFeature} from './features/public_feature';
import {I18nExpInstruction, I18nInstruction, i18nExpMapping, i18nInterpolation, i18nInterpolationV} from './i18n';
import {ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDefFlags, DirectiveDefInternal, DirectiveType, PipeDef} from './interfaces/definition';
export {ComponentFactory, ComponentFactoryResolver, ComponentRef} from './component_ref';
export {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, directiveInject, injectAttribute, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di';
export {RenderFlags} from './interfaces/definition';
@ -133,6 +137,7 @@ export {
DirectiveDefFlags,
DirectiveType,
NgOnChangesFeature,
InheritDefinitionFeature,
PublicFeature,
PipeDef,
LifecycleHooksFeature,

View File

@ -96,6 +96,12 @@ export interface DirectiveDef<T, Selector extends string> {
*/
readonly inputs: {[P in keyof T]: P};
/**
* @deprecated This is only here because `NgOnChanges` incorrectly uses declared name instead of
* public or minified name.
*/
readonly declaredInputs: {[P in keyof T]: P};
/**
* 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
@ -135,6 +141,11 @@ export interface DirectiveDef<T, Selector extends string> {
afterViewInit: (() => void)|null;
afterViewChecked: (() => void)|null;
onDestroy: (() => void)|null;
/**
* The features applied to this directive
*/
features: DirectiveDefFeature[]|null;
}
/**

View File

@ -121,6 +121,10 @@ export function compileDirective(type: Type<any>, directive: Directive): void {
}
export function extendsDirectlyFromObject(type: Type<any>): boolean {
return Object.getPrototypeOf(type.prototype) === Object.prototype;
}
/**
* Extract the `R3DirectiveMetadata` for a particular directive (either a `Directive` or a
* `Component`).
@ -136,14 +140,16 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
const inputsFromType: StringMap = {};
const outputsFromType: StringMap = {};
for (let field in propMetadata) {
propMetadata[field].forEach(ann => {
if (isInput(ann)) {
inputsFromType[field] = ann.bindingPropertyName || field;
} else if (isOutput(ann)) {
outputsFromType[field] = ann.bindingPropertyName || field;
}
});
for (const field in propMetadata) {
if (propMetadata.hasOwnProperty(field)) {
propMetadata[field].forEach(ann => {
if (isInput(ann)) {
inputsFromType[field] = ann.bindingPropertyName || field;
} else if (isOutput(ann)) {
outputsFromType[field] = ann.bindingPropertyName || field;
}
});
}
}
return {
@ -158,6 +164,7 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
usesOnChanges: type.prototype.ngOnChanges !== undefined,
},
typeSourceSpan: null !,
usesInheritance: !extendsDirectlyFromObject(type),
};
}
@ -174,14 +181,16 @@ function extractHostBindings(metadata: Directive, propMetadata: {[key: string]:
}
// Next, loop over the properties of the object, looking for @HostBinding and @HostListener.
for (let field in propMetadata) {
propMetadata[field].forEach(ann => {
if (isHostBinding(ann)) {
properties[ann.hostPropertyName || field] = field;
} else if (isHostListener(ann)) {
listeners[ann.eventName || field] = `${field}(${(ann.args || []).join(',')})`;
}
});
for (const field in propMetadata) {
if (propMetadata.hasOwnProperty(field)) {
propMetadata[field].forEach(ann => {
if (isHostBinding(ann)) {
properties[ann.hostPropertyName || field] = field;
} else if (isHostListener(ann)) {
listeners[ann.eventName || field] = `${field}(${(ann.args || []).join(',')})`;
}
});
}
}
return {attributes, listeners, properties};

View File

@ -31,6 +31,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵinjectTemplateRef': r3.injectTemplateRef,
'ɵinjectViewContainerRef': r3.injectViewContainerRef,
'ɵNgOnChangesFeature': r3.NgOnChangesFeature,
'ɵInheritDefinitionFeature': r3.InheritDefinitionFeature,
'ɵa': r3.a,
'ɵb': r3.b,
'ɵC': r3.C,