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.
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', reflector, checker);
const inputsFromFields = parseDecoratedFields(
filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), reflector, checker);
filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), reflector, checker,
resolveInput);
// And outputs.
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', reflector, checker);
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.
const contentChildFromFields = queriesFromFields(
filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), reflector,
@ -330,7 +332,8 @@ function parseFieldToPropertyMapping(
*/
function parseDecoratedFields(
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(
(results, field) => {
const fieldName = field.member.name;
@ -344,7 +347,7 @@ function parseDecoratedFields(
if (typeof property !== 'string') {
throw new Error(`Decorator argument must resolve to a string`);
}
results[fieldName] = property;
results[fieldName] = mapValueResolver(property, fieldName);
} else {
// Too many arguments.
throw new Error(
@ -353,7 +356,15 @@ function parseDecoratedFields(
});
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(

View File

@ -518,7 +518,10 @@ function tcbGetInputBindingExpressions(
// is desired. Invert `dir.inputs` into `propMatch` to create this map.
const propMatch = new Map<string, string>();
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
// matching binding.

View File

@ -1887,7 +1887,7 @@ describe('compiler compliance', () => {
type: LifecycleComp,
selectors: [["lifecycle-comp"]],
factory: function LifecycleComp_Factory(t) { return new (t || LifecycleComp)(); },
inputs: {nameMin: "name"},
inputs: {nameMin: ["name", "nameMin"]},
features: [$r3$.ɵNgOnChangesFeature],
consts: 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', () => {
const files = {
app: {

View File

@ -56,7 +56,7 @@ describe('compiler compliance: listen()', () => {
inputs:{
componentInput: "componentInput",
originalComponentInput: "renamedComponentInput"
originalComponentInput: ["renamedComponentInput", "originalComponentInput"]
},
outputs: {
componentOutput: "componentOutput",
@ -70,7 +70,7 @@ describe('compiler compliance: listen()', () => {
inputs:{
directiveInput: "directiveInput",
originalDirectiveInput: "renamedDirectiveInput"
originalDirectiveInput: ["renamedDirectiveInput", "originalDirectiveInput"]
},
outputs: {
directiveOutput: "directiveOutput",

View File

@ -86,7 +86,7 @@ export interface R3DirectiveMetadata {
/**
* 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.

View File

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

View File

@ -44,7 +44,7 @@ export interface DirectiveMeta {
*
* Goes from property names to field names.
*/
inputs: {[property: string]: string};
inputs: {[property: string]: string | string[]};
/**
* 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);
}
export function conditionallyCreateMapObjectLiteral(keys: {[key: string]: string}): o.Expression|
null {
export function conditionallyCreateMapObjectLiteral(keys: {[key: string]: string | string[]}):
o.Expression|null {
if (Object.getOwnPropertyNames(keys).length > 0) {
return mapToExpression(keys);
}

View File

@ -153,13 +153,14 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
const inputsFromMetadata = parseInputOutputs(metadata.inputs || []);
const outputsFromMetadata = parseInputOutputs(metadata.outputs || []);
const inputsFromType: StringMap = {};
const inputsFromType: {[key: string]: string | string[]} = {};
const outputsFromType: StringMap = {};
for (const field in propMetadata) {
if (propMetadata.hasOwnProperty(field)) {
propMetadata[field].forEach(ann => {
if (isInput(ann)) {
inputsFromType[field] = ann.bindingPropertyName || field;
inputsFromType[field] =
ann.bindingPropertyName ? [ann.bindingPropertyName, field] : field;
} else if (isOutput(ann)) {
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 {inject, setCurrentInjector} from '@angular/core/src/di/injector_compatibility';
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 {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');
});
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', () => {
class C {
@Input('alias1')