feat(ivy): support inheriting input/output from bare base class (#25094)

PR Close #25094
This commit is contained in:
Ben Lesh
2018-07-23 17:01:22 -07:00
committed by Igor Minar
parent 6e2a1877ab
commit 64516da6b0
9 changed files with 378 additions and 62 deletions

View File

@ -11,6 +11,7 @@ import {Provider} from '../di';
import {R3_COMPILE_COMPONENT, R3_COMPILE_DIRECTIVE, R3_COMPILE_PIPE} from '../ivy_switch';
import {Type} from '../type';
import {TypeDecorator, makeDecorator, makePropDecorator} from '../util/decorators';
import {fillProperties} from '../util/property';
import {ViewEncapsulation} from './view';
@ -734,7 +735,7 @@ export interface Input {
* selector: 'bank-account',
* template: `
* Bank Name: {{bankName}}
* Account Id: {{id}}
* Account Id: {{id}}
* `
* })
* class BankAccount {
@ -761,12 +762,47 @@ export interface Input {
bindingPropertyName?: string;
}
const initializeBaseDef = (target: any): void => {
const constructor = target.constructor;
const inheritedBaseDef = constructor.ngBaseDef;
const baseDef = constructor.ngBaseDef = {
inputs: {},
outputs: {},
declaredInputs: {},
};
if (inheritedBaseDef) {
fillProperties(baseDef.inputs, inheritedBaseDef.inputs);
fillProperties(baseDef.outputs, inheritedBaseDef.outputs);
fillProperties(baseDef.declaredInputs, inheritedBaseDef.declaredInputs);
}
};
/**
* Does the work of creating the `ngBaseDef` property for the @Input and @Output decorators.
* @param key "inputs" or "outputs"
*/
const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any}) => any) =>
(target: any, name: string, ...args: any[]) => {
const constructor = target.constructor;
if (!constructor.hasOwnProperty('ngBaseDef')) {
initializeBaseDef(target);
}
const baseDef = constructor.ngBaseDef;
const defProp = getProp(baseDef);
defProp[name] = args[0];
};
/**
*
* @Annotation
*/
export const Input: InputDecorator =
makePropDecorator('Input', (bindingPropertyName?: string) => ({bindingPropertyName}));
export const Input: InputDecorator = makePropDecorator(
'Input', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined,
updateBaseDefFromIOProp(baseDef => baseDef.inputs || {}));
/**
* Type of the Output decorator / constructor function.
@ -800,8 +836,10 @@ export interface Output { bindingPropertyName?: string; }
*
* @Annotation
*/
export const Output: OutputDecorator =
makePropDecorator('Output', (bindingPropertyName?: string) => ({bindingPropertyName}));
export const Output: OutputDecorator = makePropDecorator(
'Output', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined,
updateBaseDefFromIOProp(baseDef => baseDef.outputs || {}));
/**

View File

@ -16,7 +16,7 @@ import {Type} from '../type';
import {resolveRendererType2} from '../view/util';
import {diPublic} from './di';
import {ComponentDefFeature, ComponentDefInternal, ComponentQuery, ComponentTemplate, ComponentType, DirectiveDefFeature, DirectiveDefInternal, DirectiveDefListOrFactory, DirectiveType, DirectiveTypesOrFactory, PipeDefInternal, PipeType, PipeTypesOrFactory} from './interfaces/definition';
import {BaseDef, ComponentDefFeature, ComponentDefInternal, ComponentQuery, ComponentTemplate, ComponentType, DirectiveDefFeature, DirectiveDefInternal, DirectiveDefListOrFactory, DirectiveType, DirectiveTypesOrFactory, PipeDefInternal, PipeType, PipeTypesOrFactory} from './interfaces/definition';
import {CssSelectorList, SelectorFlags} from './interfaces/projection';
@ -353,6 +353,84 @@ function invertObject(obj: any, secondary?: any): any {
return newLookup;
}
/**
* Create a base definition
*
* # Example
* ```
* class ShouldBeInherited {
* static ngBaseDef = defineBase({
* ...
* })
* }
* @param baseDefinition The base definition parameters
*/
export function defineBase<T>(baseDefinition: {
/**
* A map of input names.
*
* The format is in: `{[actualPropertyName: string]:(string|[string, string])}`.
*
* Given:
* ```
* class MyComponent {
* @Input()
* publicInput1: string;
*
* @Input('publicInput2')
* declaredInput2: string;
* }
* ```
*
* is described as:
* ```
* {
* publicInput1: 'publicInput1',
* declaredInput2: ['declaredInput2', 'publicInput2'],
* }
* ```
*
* Which the minifier may translate to:
* ```
* {
* minifiedPublicInput1: 'publicInput1',
* minifiedDeclaredInput2: [ 'declaredInput2', 'publicInput2'],
* }
* ```
*
* 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 `outputs`.
*/
inputs?: {[P in keyof T]?: string | [string, string]};
/**
* A map of output names.
*
* The format is in: `{[actualPropertyName: string]:string}`.
*
* Which the minifier may translate to: `{[minifiedPropertyName: string]:string}`.
*
* This allows the render to re-construct the minified and non-minified names
* of properties.
*/
outputs?: {[P in keyof T]?: string};
}): BaseDef<T> {
const declaredInputs: {[P in keyof T]: P} = {} as any;
return {
inputs: invertObject(baseDefinition.inputs, declaredInputs),
declaredInputs: declaredInputs,
outputs: invertObject(baseDefinition.outputs),
};
}
/**
* Create a directive definition object.
*

View File

@ -7,22 +7,10 @@
*/
import {Type} from '../../type';
import {ComponentDefInternal, ComponentType, DirectiveDefFeature, DirectiveDefInternal} from '../interfaces/definition';
import {fillProperties} from '../../util/property';
import {ComponentDefInternal, 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
@ -45,9 +33,9 @@ function getSuperType(type: Type<any>): Type<any>&
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) {
while (superType) {
let superDef: DirectiveDefInternal<any>|ComponentDefInternal<any>|undefined = undefined;
if (isComponentDef(definition)) {
superDef = superType.ngComponentDef || superType.ngDirectiveDef;
} else {
@ -57,12 +45,15 @@ export function InheritDefinitionFeature(
superDef = superType.ngDirectiveDef;
}
if (superDef) {
const baseDef = (superType as any).ngBaseDef;
if (baseDef) {
// Merge inputs and outputs
fillProperties(definition.inputs, superDef.inputs);
fillProperties(definition.declaredInputs, superDef.declaredInputs);
fillProperties(definition.outputs, superDef.outputs);
fillProperties(definition.inputs, baseDef.inputs);
fillProperties(definition.declaredInputs, baseDef.declaredInputs);
fillProperties(definition.outputs, baseDef.outputs);
}
if (superDef) {
// Merge hostBindings
const prevHostBindings = definition.hostBindings;
const superHostBindings = superDef.hostBindings;
@ -77,6 +68,11 @@ export function InheritDefinitionFeature(
}
}
// Merge inputs and outputs
fillProperties(definition.inputs, superDef.inputs);
fillProperties(definition.declaredInputs, superDef.declaredInputs);
fillProperties(definition.outputs, superDef.outputs);
// Inherit hooks
// Assume super class inheritance feature has already run.
definition.afterContentChecked =
@ -97,6 +93,8 @@ export function InheritDefinitionFeature(
}
}
}
break;
} 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;

View File

@ -7,7 +7,7 @@
*/
import {LifecycleHooksFeature, getHostElement, getRenderedText, renderComponent, whenRendered} from './component';
import {defineComponent, defineDirective, defineNgModule, definePipe} from './definition';
import {defineBase, 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';
@ -164,6 +164,7 @@ export {
defineComponent,
defineDirective,
defineNgModule,
defineBase,
definePipe,
getHostElement,
getRenderedText,

View File

@ -67,29 +67,15 @@ export interface PipeType<T> extends Type<T> { ngPipeDef: never; }
export type DirectiveDefInternal<T> = DirectiveDef<T, string>;
/**
* Runtime link information for Directives.
* Runtime information for classes that are inherited by components or directives
* that aren't defined as components or directives.
*
* This is internal data structure used by the render to link
* directives into templates.
* This is an internal data structure used by the render to determine what inputs
* and outputs should be inherited.
*
* NOTE: Always use `defineDirective` function to create this object,
* never create the object directly since the shape of this object
* can change between versions.
*
* @param Selector type metadata specifying the selector of the directive or component
*
* See: {@link defineDirective}
* See: {@link defineBase}
*/
export interface DirectiveDef<T, Selector extends string> {
/** Token representing the directive. Used by DI. */
type: Type<T>;
/** Function that makes a directive public to the DI system. */
diPublic: ((def: DirectiveDef<T, string>) => void)|null;
/** The selectors that will be used to match nodes to this directive. */
selectors: CssSelectorList;
export interface BaseDef<T> {
/**
* A dictionary mapping the inputs' minified property names to their public API names, which
* are their aliases if any, or their original unminified property names
@ -109,6 +95,31 @@ export interface DirectiveDef<T, Selector extends string> {
* (as in `@Output('alias') propertyName: any;`).
*/
readonly outputs: {[P in keyof T]: P};
}
/**
* Runtime link information for Directives.
*
* This is internal data structure used by the render to link
* directives into templates.
*
* NOTE: Always use `defineDirective` function to create this object,
* never create the object directly since the shape of this object
* can change between versions.
*
* @param Selector type metadata specifying the selector of the directive or component
*
* See: {@link defineDirective}
*/
export interface DirectiveDef<T, Selector extends string> extends BaseDef<T> {
/** Token representing the directive. Used by DI. */
type: Type<T>;
/** Function that makes a directive public to the DI system. */
diPublic: ((def: DirectiveDef<T, string>) => void)|null;
/** The selectors that will be used to match nodes to this directive. */
selectors: CssSelectorList;
/**
* Name under which the directive is exported (for use with local references in template)

View File

@ -41,31 +41,34 @@ export const PROP_METADATA = '__prop__metadata__';
/**
* @suppress {globalThis}
*/
export function makeDecorator(
export function makeDecorator<T>(
name: string, props?: (...args: any[]) => any, parentClass?: any,
chainFn?: (fn: Function) => void, typeFn?: (type: Type<any>, ...args: any[]) => void):
additionalProcessing?: (type: Type<T>) => void,
typeFn?: (type: Type<T>, ...args: any[]) => void):
{new (...args: any[]): any; (...args: any[]): any; (...args: any[]): (cls: any) => any;} {
const metaCtor = makeMetadataCtor(props);
function DecoratorFactory(...args: any[]): (cls: any) => any {
function DecoratorFactory(...args: any[]): (cls: Type<T>) => any {
if (this instanceof DecoratorFactory) {
metaCtor.call(this, ...args);
return this;
}
const annotationInstance = new (<any>DecoratorFactory)(...args);
const TypeDecorator: TypeDecorator = <TypeDecorator>function TypeDecorator(cls: Type<any>) {
typeFn && typeFn(cls, ...args);
const annotationInstance = new (DecoratorFactory as any)(...args);
return function TypeDecorator(cls: Type<T>) {
if (typeFn) typeFn(cls, ...args);
// Use of Object.defineProperty is important since it creates non-enumerable property which
// prevents the property is copied during subclassing.
const annotations = cls.hasOwnProperty(ANNOTATIONS) ?
(cls as any)[ANNOTATIONS] :
Object.defineProperty(cls, ANNOTATIONS, {value: []})[ANNOTATIONS];
annotations.push(annotationInstance);
if (additionalProcessing) additionalProcessing(cls);
return cls;
};
if (chainFn) chainFn(TypeDecorator);
return TypeDecorator;
}
if (parentClass) {
@ -73,7 +76,7 @@ export function makeDecorator(
}
DecoratorFactory.prototype.ngMetadataName = name;
(<any>DecoratorFactory).annotationCls = DecoratorFactory;
(DecoratorFactory as any).annotationCls = DecoratorFactory;
return DecoratorFactory as any;
}
@ -127,7 +130,8 @@ export function makeParamDecorator(
}
export function makePropDecorator(
name: string, props?: (...args: any[]) => any, parentClass?: any): any {
name: string, props?: (...args: any[]) => any, parentClass?: any,
additionalProcessing?: (target: any, name: string, ...args: any[]) => void): any {
const metaCtor = makeMetadataCtor(props);
function PropDecoratorFactory(...args: any[]): any {
@ -138,7 +142,7 @@ export function makePropDecorator(
const decoratorInstance = new (<any>PropDecoratorFactory)(...args);
return function PropDecorator(target: any, name: string) {
function PropDecorator(target: any, name: string) {
const constructor = target.constructor;
// Use of Object.defineProperty is important since it creates non-enumerable property which
// prevents the property is copied during subclassing.
@ -147,7 +151,11 @@ export function makePropDecorator(
Object.defineProperty(constructor, PROP_METADATA, {value: {}})[PROP_METADATA];
meta[name] = meta.hasOwnProperty(name) && meta[name] || [];
meta[name].unshift(decoratorInstance);
};
if (additionalProcessing) additionalProcessing(target, name, ...args);
}
return PropDecorator;
}
if (parentClass) {

View File

@ -14,3 +14,17 @@ export function getClosureSafeProperty<T>(objWithPropertyToExtract: T, target: a
}
throw Error('Could not find renamed property on target object.');
}
/**
* 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
*/
export 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];
}
}
}