From 19fcfc3d00d863b7ff444d1e7e538c9428df0841 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Thu, 25 Oct 2018 23:05:15 -0700 Subject: [PATCH] fix(compiler): generate inputs with aliases properly (#26774) PR Close #26774 --- .../src/ngtsc/annotations/src/directive.ts | 21 ++++++++++---- .../ngtsc/typecheck/src/type_check_block.ts | 5 +++- .../compliance/r3_compiler_compliance_spec.ts | 4 +-- .../r3_view_compiler_input_outputs_spec.ts | 6 ++-- packages/compiler/src/render3/view/api.ts | 2 +- .../compiler/src/render3/view/compiler.ts | 15 ++++++---- packages/compiler/src/render3/view/t2_api.ts | 2 +- packages/compiler/src/render3/view/util.ts | 4 +-- packages/core/src/render3/jit/directive.ts | 5 ++-- packages/core/test/render3/ivy/jit_spec.ts | 29 ++++++++++++++++++- 10 files changed, 69 insertions(+), 24 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 8bd57318f3..bec4029165 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -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( diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 58f8fa4803..c37d7a10e5 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -518,7 +518,10 @@ function tcbGetInputBindingExpressions( // is desired. Invert `dir.inputs` into `propMatch` to create this map. const propMatch = new Map(); 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. diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 68d1454ddb..4d3f8f6fca 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -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: { diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_input_outputs_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_input_outputs_spec.ts index 06b57717af..a801382f65 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_input_outputs_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_input_outputs_spec.ts @@ -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", @@ -86,4 +86,4 @@ describe('compiler compliance: listen()', () => { expectEmit(result.source, directiveDef, 'Incorrect directive definition'); }); -}); \ No newline at end of file +}); diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index e92454cc8d..192135f73b 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -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. diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index ae7de11467..556a9a6b96 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -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 => ({ - key, - value: o.literal(map[key]), - quoted: true, - })); +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(value), + quoted: true, + }; + }); return o.expressionType(o.literalMap(mapValues)); } diff --git a/packages/compiler/src/render3/view/t2_api.ts b/packages/compiler/src/render3/view/t2_api.ts index 91cb594a61..4d6adcae71 100644 --- a/packages/compiler/src/render3/view/t2_api.ts +++ b/packages/compiler/src/render3/view/t2_api.ts @@ -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. diff --git a/packages/compiler/src/render3/view/util.ts b/packages/compiler/src/render3/view/util.ts index 30996f47c9..d5da1b807c 100644 --- a/packages/compiler/src/render3/view/util.ts +++ b/packages/compiler/src/render3/view/util.ts @@ -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); } diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index f82aef121e..76cf70d0cd 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -153,13 +153,14 @@ function directiveMetadata(type: Type, 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; } diff --git a/packages/core/test/render3/ivy/jit_spec.ts b/packages/core/test/render3/ivy/jit_spec.ts index cdca28fe08..382b571f2a 100644 --- a/packages/core/test/render3/ivy/jit_spec.ts +++ b/packages/core/test/render3/ivy/jit_spec.ts @@ -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')