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[];