fix(compiler): generate inputs with aliases properly (#26774)

PR Close #26774
This commit is contained in:
Kara Erickson 2018-10-25 23:05:15 -07:00 committed by Matias Niemelä
parent c048358cf9
commit 19fcfc3d00
10 changed files with 69 additions and 24 deletions

View File

@ -110,12 +110,14 @@ export function extractDirectiveMetadata(
// fields. // fields.
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', reflector, checker); const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', reflector, checker);
const inputsFromFields = parseDecoratedFields( const inputsFromFields = parseDecoratedFields(
filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), reflector, checker); filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), reflector, checker,
resolveInput);
// And outputs. // And outputs.
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', reflector, checker); const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', reflector, checker);
const outputsFromFields = parseDecoratedFields( const outputsFromFields = parseDecoratedFields(
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), reflector, checker); filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), reflector, checker,
resolveOutput) as{[field: string]: string};
// Construct the list of queries. // Construct the list of queries.
const contentChildFromFields = queriesFromFields( const contentChildFromFields = queriesFromFields(
filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), reflector, filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), reflector,
@ -330,7 +332,8 @@ function parseFieldToPropertyMapping(
*/ */
function parseDecoratedFields( function parseDecoratedFields(
fields: {member: ClassMember, decorators: Decorator[]}[], reflector: ReflectionHost, fields: {member: ClassMember, decorators: Decorator[]}[], reflector: ReflectionHost,
checker: ts.TypeChecker): {[field: string]: string} { checker: ts.TypeChecker, mapValueResolver: (publicName: string, internalName: string) =>
string | string[]): {[field: string]: string | string[]} {
return fields.reduce( return fields.reduce(
(results, field) => { (results, field) => {
const fieldName = field.member.name; const fieldName = field.member.name;
@ -344,7 +347,7 @@ function parseDecoratedFields(
if (typeof property !== 'string') { if (typeof property !== 'string') {
throw new Error(`Decorator argument must resolve to a string`); throw new Error(`Decorator argument must resolve to a string`);
} }
results[fieldName] = property; results[fieldName] = mapValueResolver(property, fieldName);
} else { } else {
// Too many arguments. // Too many arguments.
throw new Error( throw new Error(
@ -353,7 +356,15 @@ function parseDecoratedFields(
}); });
return results; return results;
}, },
{} as{[field: string]: string}); {} as{[field: string]: string | string[]});
}
function resolveInput(publicName: string, internalName: string) {
return [publicName, internalName];
}
function resolveOutput(publicName: string, internalName: string) {
return publicName;
} }
export function queriesFromFields( export function queriesFromFields(

View File

@ -518,7 +518,10 @@ function tcbGetInputBindingExpressions(
// is desired. Invert `dir.inputs` into `propMatch` to create this map. // is desired. Invert `dir.inputs` into `propMatch` to create this map.
const propMatch = new Map<string, string>(); const propMatch = new Map<string, string>();
const inputs = dir.inputs; const inputs = dir.inputs;
Object.keys(inputs).forEach(key => propMatch.set(inputs[key], key)); Object.keys(inputs).forEach(key => {
Array.isArray(inputs[key]) ? propMatch.set(inputs[key][0], key) :
propMatch.set(inputs[key] as string, key);
});
// Add a binding expression to the map for each input of the directive that has a // Add a binding expression to the map for each input of the directive that has a
// matching binding. // matching binding.

View File

@ -1887,7 +1887,7 @@ describe('compiler compliance', () => {
type: LifecycleComp, type: LifecycleComp,
selectors: [["lifecycle-comp"]], selectors: [["lifecycle-comp"]],
factory: function LifecycleComp_Factory(t) { return new (t || LifecycleComp)(); }, factory: function LifecycleComp_Factory(t) { return new (t || LifecycleComp)(); },
inputs: {nameMin: "name"}, inputs: {nameMin: ["name", "nameMin"]},
features: [$r3$.ɵNgOnChangesFeature], features: [$r3$.ɵNgOnChangesFeature],
consts: 0, consts: 0,
vars: 0, vars: 0,
@ -2301,7 +2301,7 @@ describe('compiler compliance', () => {
}); });
}); });
describe('inherited bare classes', () => { describe('inherited base classes', () => {
it('should add ngBaseDef if one or more @Input is present', () => { it('should add ngBaseDef if one or more @Input is present', () => {
const files = { const files = {
app: { app: {

View File

@ -56,7 +56,7 @@ describe('compiler compliance: listen()', () => {
inputs:{ inputs:{
componentInput: "componentInput", componentInput: "componentInput",
originalComponentInput: "renamedComponentInput" originalComponentInput: ["renamedComponentInput", "originalComponentInput"]
}, },
outputs: { outputs: {
componentOutput: "componentOutput", componentOutput: "componentOutput",
@ -70,7 +70,7 @@ describe('compiler compliance: listen()', () => {
inputs:{ inputs:{
directiveInput: "directiveInput", directiveInput: "directiveInput",
originalDirectiveInput: "renamedDirectiveInput" originalDirectiveInput: ["renamedDirectiveInput", "originalDirectiveInput"]
}, },
outputs: { outputs: {
directiveOutput: "directiveOutput", directiveOutput: "directiveOutput",
@ -86,4 +86,4 @@ describe('compiler compliance: listen()', () => {
expectEmit(result.source, directiveDef, 'Incorrect directive definition'); expectEmit(result.source, directiveDef, 'Incorrect directive definition');
}); });
}); });

View File

@ -86,7 +86,7 @@ export interface R3DirectiveMetadata {
/** /**
* A mapping of input field names to the property names. * A mapping of input field names to the property names.
*/ */
inputs: {[field: string]: string}; inputs: {[field: string]: string | string[]};
/** /**
* A mapping of output field names to the property names. * A mapping of output field names to the property names.

View File

@ -532,12 +532,15 @@ function stringAsType(str: string): o.Type {
return o.expressionType(o.literal(str)); return o.expressionType(o.literal(str));
} }
function stringMapAsType(map: {[key: string]: string}): o.Type { function stringMapAsType(map: {[key: string]: string | string[]}): o.Type {
const mapValues = Object.keys(map).map(key => ({ const mapValues = Object.keys(map).map(key => {
key, const value = Array.isArray(map[key]) ? map[key][0] : map[key];
value: o.literal(map[key]), return {
quoted: true, key,
})); value: o.literal(value),
quoted: true,
};
});
return o.expressionType(o.literalMap(mapValues)); return o.expressionType(o.literalMap(mapValues));
} }

View File

@ -44,7 +44,7 @@ export interface DirectiveMeta {
* *
* Goes from property names to field names. * Goes from property names to field names.
*/ */
inputs: {[property: string]: string}; inputs: {[property: string]: string | string[]};
/** /**
* Set of outputs which this directive claims. * Set of outputs which this directive claims.

View File

@ -67,8 +67,8 @@ export function asLiteral(value: any): o.Expression {
return o.literal(value, o.INFERRED_TYPE); return o.literal(value, o.INFERRED_TYPE);
} }
export function conditionallyCreateMapObjectLiteral(keys: {[key: string]: string}): o.Expression| export function conditionallyCreateMapObjectLiteral(keys: {[key: string]: string | string[]}):
null { o.Expression|null {
if (Object.getOwnPropertyNames(keys).length > 0) { if (Object.getOwnPropertyNames(keys).length > 0) {
return mapToExpression(keys); return mapToExpression(keys);
} }

View File

@ -153,13 +153,14 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
const inputsFromMetadata = parseInputOutputs(metadata.inputs || []); const inputsFromMetadata = parseInputOutputs(metadata.inputs || []);
const outputsFromMetadata = parseInputOutputs(metadata.outputs || []); const outputsFromMetadata = parseInputOutputs(metadata.outputs || []);
const inputsFromType: StringMap = {}; const inputsFromType: {[key: string]: string | string[]} = {};
const outputsFromType: StringMap = {}; const outputsFromType: StringMap = {};
for (const field in propMetadata) { for (const field in propMetadata) {
if (propMetadata.hasOwnProperty(field)) { if (propMetadata.hasOwnProperty(field)) {
propMetadata[field].forEach(ann => { propMetadata[field].forEach(ann => {
if (isInput(ann)) { if (isInput(ann)) {
inputsFromType[field] = ann.bindingPropertyName || field; inputsFromType[field] =
ann.bindingPropertyName ? [ann.bindingPropertyName, field] : field;
} else if (isOutput(ann)) { } else if (isOutput(ann)) {
outputsFromType[field] = ann.bindingPropertyName || field; outputsFromType[field] = ann.bindingPropertyName || field;
} }

View File

@ -12,7 +12,7 @@ import {InjectorDef, defineInjectable} from '@angular/core/src/di/defs';
import {Injectable} from '@angular/core/src/di/injectable'; import {Injectable} from '@angular/core/src/di/injectable';
import {inject, setCurrentInjector} from '@angular/core/src/di/injector_compatibility'; import {inject, setCurrentInjector} from '@angular/core/src/di/injector_compatibility';
import {ivyEnabled} from '@angular/core/src/ivy_switch'; import {ivyEnabled} from '@angular/core/src/ivy_switch';
import {Component, HostBinding, HostListener, Input, Output, Pipe} from '@angular/core/src/metadata/directives'; import {Component, Directive, HostBinding, HostListener, Input, Output, Pipe} from '@angular/core/src/metadata/directives';
import {NgModule, NgModuleDef} from '@angular/core/src/metadata/ng_module'; import {NgModule, NgModuleDef} from '@angular/core/src/metadata/ng_module';
import {ComponentDef, PipeDef} from '@angular/core/src/render3/interfaces/definition'; import {ComponentDef, PipeDef} from '@angular/core/src/render3/interfaces/definition';
@ -252,6 +252,33 @@ ivyEnabled && describe('render3 jit', () => {
expect(pipeDef.pure).toBe(true, 'pipe should be pure'); expect(pipeDef.pure).toBe(true, 'pipe should be pure');
}); });
it('should add @Input properties to a component', () => {
@Component({
selector: 'input-comp',
template: 'test',
})
class InputComp {
@Input('publicName') privateName = 'name1';
}
const InputCompAny = InputComp as any;
expect(InputCompAny.ngComponentDef.inputs).toEqual({publicName: 'privateName'});
expect(InputCompAny.ngComponentDef.declaredInputs).toEqual({privateName: 'privateName'});
});
it('should add @Input properties to a directive', () => {
@Directive({
selector: '[dir]',
})
class InputDir {
@Input('publicName') privateName = 'name1';
}
const InputDirAny = InputDir as any;
expect(InputDirAny.ngDirectiveDef.inputs).toEqual({publicName: 'privateName'});
expect(InputDirAny.ngDirectiveDef.declaredInputs).toEqual({privateName: 'privateName'});
});
it('should add ngBaseDef to types with @Input properties', () => { it('should add ngBaseDef to types with @Input properties', () => {
class C { class C {
@Input('alias1') @Input('alias1')