@@ -11,7 +11,7 @@ import ts from 'typescript';
1111
1212import { ErrorCode , FatalDiagnosticError } from '../../../diagnostics' ;
1313import { Reference , ReferenceEmitter } from '../../../imports' ;
14- import { ClassPropertyMapping , HostDirectiveMeta } from '../../../metadata' ;
14+ import { ClassPropertyMapping , HostDirectiveMeta , InputMapping } from '../../../metadata' ;
1515import { DynamicValue , EnumValue , PartialEvaluator , ResolvedValue } from '../../../partial_evaluator' ;
1616import { ClassDeclaration , ClassMember , ClassMemberKind , Decorator , filterToMembersWithDecorator , isNamedClassDeclaration , ReflectionHost , reflectObjectLiteral } from '../../../reflection' ;
1717import { HandlerFlags } from '../../../transform' ;
@@ -37,7 +37,7 @@ export function extractDirectiveMetadata(
3737 annotateForClosureCompiler : boolean , defaultSelector : string | null = null ) : {
3838 decorator : Map < string , ts . Expression > ,
3939 metadata : R3DirectiveMetadata ,
40- inputs : ClassPropertyMapping ,
40+ inputs : ClassPropertyMapping < InputMapping > ,
4141 outputs : ClassPropertyMapping ,
4242 isStructural : boolean ;
4343 hostDirectives : HostDirectiveMeta [ ] | null , rawHostDirectives : ts . Expression | null ,
@@ -74,19 +74,18 @@ export function extractDirectiveMetadata(
7474 const coreModule = isCore ? undefined : '@angular/core' ;
7575
7676 // Construct the map of inputs both from the @Directive /@Component
77- // decorator, and the decorated
78- // fields.
79- const inputsFromMeta = parseFieldToPropertyMapping ( directive , 'inputs' , evaluator ) ;
80- const inputsFromFields = parseDecoratedFields (
81- filterToMembersWithDecorator ( decoratedElements , 'Input' , coreModule ) , evaluator ,
82- resolveInput ) ;
77+ // decorator, and the decorated fields.
78+ const inputsFromMeta = parseInputsArray ( directive , evaluator ) ;
79+ const inputsFromFields = parseInputFields (
80+ filterToMembersWithDecorator ( decoratedElements , 'Input' , coreModule ) , evaluator ) ;
81+ const inputs = ClassPropertyMapping . fromMappedObject ( { ...inputsFromMeta , ...inputsFromFields } ) ;
8382
8483 // And outputs.
85- const outputsFromMeta = parseFieldToPropertyMapping ( directive , 'outputs' , evaluator ) ;
86- const outputsFromFields =
87- parseDecoratedFields (
88- filterToMembersWithDecorator ( decoratedElements , 'Output' , coreModule ) , evaluator ,
89- resolveOutput ) as { [ field : string ] : string } ;
84+ const outputsFromMeta = parseOutputsArray ( directive , evaluator ) ;
85+ const outputsFromFields = parseOutputFields (
86+ filterToMembersWithDecorator ( decoratedElements , 'Output' , coreModule ) , evaluator ) ;
87+ const outputs = ClassPropertyMapping . fromMappedObject ( { ... outputsFromMeta , ... outputsFromFields } ) ;
88+
9089 // Construct the list of queries.
9190 const contentChildFromFields = queriesFromFields (
9291 filterToMembersWithDecorator ( decoratedElements , 'ContentChild' , coreModule ) , reflector ,
@@ -185,8 +184,6 @@ export function extractDirectiveMetadata(
185184 const sourceFile = clazz . getSourceFile ( ) ;
186185 const type = wrapTypeReference ( reflector , clazz ) ;
187186 const internalType = new WrappedNodeExpr ( reflector . getInternalNameOfClass ( clazz ) ) ;
188- const inputs = ClassPropertyMapping . fromMappedObject ( { ...inputsFromMeta , ...inputsFromFields } ) ;
189- const outputs = ClassPropertyMapping . fromMappedObject ( { ...outputsFromMeta , ...outputsFromFields } ) ;
190187 const rawHostDirectives = directive . get ( 'hostDirectives' ) || null ;
191188 const hostDirectives =
192189 rawHostDirectives === null ? null : extractHostDirectives ( rawHostDirectives , evaluator ) ;
@@ -448,7 +445,7 @@ function extractQueriesFromDecorator(
448445 return { content, view} ;
449446}
450447
451- export function parseFieldArrayValue (
448+ export function parseFieldStringArrayValue (
452449 directive : Map < string , ts . Expression > , field : string , evaluator : PartialEvaluator ) : null |
453450 string [ ] {
454451 if ( ! directive . has ( field ) ) {
@@ -512,76 +509,138 @@ function isPropertyTypeMember(member: ClassMember): boolean {
512509 member . kind === ClassMemberKind . Property ;
513510}
514511
515- /**
516- * Interpret property mapping fields on the decorator (e.g. inputs or outputs) and return the
517- * correctly shaped metadata object.
518- */
519- function parseFieldToPropertyMapping (
520- directive : Map < string , ts . Expression > , field : string ,
521- evaluator : PartialEvaluator ) : { [ field : string ] : string } {
522- const metaValues = parseFieldArrayValue ( directive , field , evaluator ) ;
523- return metaValues ? parseInputOutputMappingArray ( metaValues ) : EMPTY_OBJECT ;
524- }
525-
526- function parseInputOutputMappingArray ( values : string [ ] ) {
512+ function parseMappingStringArray ( values : string [ ] ) {
527513 return values . reduce ( ( results , value ) => {
528514 if ( typeof value !== 'string' ) {
529515 throw new Error ( 'Mapping value must be a string' ) ;
530516 }
531517
532- // Either the value is 'field' or 'field: property'. In the first case, `property` will
533- // be undefined, in which case the field name should also be used as the property name.
534- const [ field , property ] = value . split ( ':' , 2 ) . map ( str => str . trim ( ) ) ;
535- results [ field ] = property || field ;
518+ const [ bindingPropertyName , fieldName ] = parseMappingString ( value ) ;
519+ results [ fieldName ] = bindingPropertyName ;
536520 return results ;
537521 } , { } as { [ field : string ] : string } ) ;
538522}
539523
524+ function parseMappingString ( value : string ) : [ bindingPropertyName : string , fieldName : string ] {
525+ // Either the value is 'field' or 'field: property'. In the first case, `property` will
526+ // be undefined, in which case the field name should also be used as the property name.
527+ const [ fieldName , bindingPropertyName ] = value . split ( ':' , 2 ) . map ( str => str . trim ( ) ) ;
528+ return [ bindingPropertyName ?? fieldName , fieldName ] ;
529+ }
530+
540531/**
541- * Parse property decorators (e.g. `Input` or `Output`) and return the correctly shaped metadata
542- * object.
532+ * Parse property decorators (e.g. `Input` or `Output`) and invoke callback with the parsed data.
543533 */
544534function parseDecoratedFields (
545535 fields : { member : ClassMember , decorators : Decorator [ ] } [ ] , evaluator : PartialEvaluator ,
546- mapValueResolver : ( publicName : string , internalName : string ) =>
547- string | [ string , string ] ) : { [ field : string ] : string | [ string , string ] } {
548- return fields . reduce ( ( results , field ) => {
536+ callback : ( fieldName : string , fieldValue : ResolvedValue , decorator : Decorator ) => void ) : void {
537+ for ( const field of fields ) {
549538 const fieldName = field . member . name ;
550- field . decorators . forEach ( decorator => {
551- // The decorator either doesn't have an argument (@Input()) in which case the property
552- // name is used, or it has one argument (@Output('named')).
553- if ( decorator . args == null || decorator . args . length === 0 ) {
554- results [ fieldName ] = fieldName ;
555- } else if ( decorator . args . length === 1 ) {
556- const property = evaluator . evaluate ( decorator . args [ 0 ] ) ;
557- if ( typeof property !== 'string' ) {
558- throw createValueHasWrongTypeError (
559- Decorator . nodeForError ( decorator ) , property ,
560- `@${ decorator . name } decorator argument must resolve to a string` ) ;
561- }
562- results [ fieldName ] = mapValueResolver ( property , fieldName ) ;
563- } else {
564- // Too many arguments.
539+
540+ for ( const decorator of field . decorators ) {
541+ if ( decorator . args != null && decorator . args . length > 1 ) {
565542 throw new FatalDiagnosticError (
566543 ErrorCode . DECORATOR_ARITY_WRONG , Decorator . nodeForError ( decorator ) ,
567544 `@${ decorator . name } can have at most one argument, got ${
568545 decorator . args . length } argument(s)`) ;
569546 }
570- } ) ;
571- return results ;
572- } , { } as { [ field : string ] : string | [ string , string ] } ) ;
547+
548+ const value = decorator . args != null && decorator . args . length > 0 ?
549+ evaluator . evaluate ( decorator . args [ 0 ] ) :
550+ null ;
551+
552+ callback ( fieldName , value , decorator ) ;
553+ }
554+ }
573555}
574556
575- function resolveInput ( publicName : string , internalName : string ) : [ string , string ] {
576- return [ publicName , internalName ] ;
557+ /** Parses the `inputs` array of a directive/component decorator. */
558+ function parseInputsArray (
559+ decoratorMetadata : Map < string , ts . Expression > ,
560+ evaluator : PartialEvaluator ) : Record < string , InputMapping > {
561+ const inputsField = decoratorMetadata . get ( 'inputs' ) ;
562+
563+ if ( inputsField === undefined ) {
564+ return { } ;
565+ }
566+
567+ const inputs = { } as Record < string , InputMapping > ;
568+ const inputsArray = evaluator . evaluate ( inputsField ) ;
569+
570+ // TODO(required-inputs): change the error wording here
571+ if ( ! Array . isArray ( inputsArray ) ) {
572+ throw createValueHasWrongTypeError (
573+ inputsField , inputsArray , `Failed to resolve @Directive.inputs to a string array` ) ;
574+ }
575+
576+ for ( const value of inputsArray ) {
577+ // TODO(required-inputs): parse object-based config.
578+ if ( typeof value === 'string' ) {
579+ // If the value is a string, we treat it as a mapping string.
580+ const [ bindingPropertyName , fieldName ] = parseMappingString ( value ) ;
581+ inputs [ fieldName ] = { bindingPropertyName, classPropertyName : fieldName , required : false } ;
582+ } else {
583+ // TODO(required-inputs): change the error wording here
584+ throw createValueHasWrongTypeError (
585+ inputsField , value , `Failed to resolve @Directive.inputs to a string array` ) ;
586+ }
587+ }
588+
589+ return inputs ;
577590}
578591
579- function resolveOutput ( publicName : string , internalName : string ) {
580- return publicName ;
592+ /** Parses the class members that are decorated as inputs. */
593+ function parseInputFields (
594+ inputMembers : { member : ClassMember , decorators : Decorator [ ] } [ ] ,
595+ evaluator : PartialEvaluator ) : Record < string , InputMapping > {
596+ const inputs = { } as Record < string , InputMapping > ;
597+
598+ parseDecoratedFields ( inputMembers , evaluator , ( classPropertyName , options , decorator ) => {
599+ let bindingPropertyName : string ;
600+
601+ // TODO(required-inputs): parse object-based config.
602+ if ( options === null ) {
603+ bindingPropertyName = classPropertyName ;
604+ } else if ( typeof options === 'string' ) {
605+ bindingPropertyName = options ;
606+ } else {
607+ // TODO(required-inputs): change the error wording here
608+ throw createValueHasWrongTypeError (
609+ Decorator . nodeForError ( decorator ) , options ,
610+ `@${ decorator . name } decorator argument must resolve to a string` ) ;
611+ }
612+
613+ inputs [ classPropertyName ] = { bindingPropertyName, classPropertyName, required : false } ;
614+ } ) ;
615+
616+ return inputs ;
617+ }
618+
619+ /** Parses the `outputs` array of a directive/component. */
620+ function parseOutputsArray (
621+ directive : Map < string , ts . Expression > , evaluator : PartialEvaluator ) : Record < string , string > {
622+ const metaValues = parseFieldStringArrayValue ( directive , 'outputs' , evaluator ) ;
623+ return metaValues ? parseMappingStringArray ( metaValues ) : EMPTY_OBJECT ;
624+ }
625+
626+ /** Parses the class members that are decorated as outputs. */
627+ function parseOutputFields (
628+ outputMembers : { member : ClassMember , decorators : Decorator [ ] } [ ] ,
629+ evaluator : PartialEvaluator ) : Record < string , string > {
630+ const outputs = { } as Record < string , string > ;
631+
632+ parseDecoratedFields ( outputMembers , evaluator , ( fieldName , bindingPropertyName , decorator ) => {
633+ if ( bindingPropertyName != null && typeof bindingPropertyName !== 'string' ) {
634+ throw createValueHasWrongTypeError (
635+ Decorator . nodeForError ( decorator ) , bindingPropertyName ,
636+ `@${ decorator . name } decorator argument must resolve to a string` ) ;
637+ }
638+
639+ outputs [ fieldName ] = bindingPropertyName ?? fieldName ;
640+ } ) ;
641+
642+ return outputs ;
581643}
582- type StringMap < T > = {
583- [ key : string ] : T ;
584- } ;
585644
586645function evaluateHostExpressionBindings (
587646 hostExpr : ts . Expression , evaluator : PartialEvaluator ) : ParsedHostBindings {
@@ -590,7 +649,7 @@ function evaluateHostExpressionBindings(
590649 throw createValueHasWrongTypeError (
591650 hostExpr , hostMetaMap , `Decorator host metadata must be an object` ) ;
592651 }
593- const hostMetadata : StringMap < string | Expression > = { } ;
652+ const hostMetadata : Record < string , string | Expression > = { } ;
594653 hostMetaMap . forEach ( ( value , key ) => {
595654 // Resolve Enum references to their declared value.
596655 if ( value instanceof EnumValue ) {
@@ -673,13 +732,13 @@ function extractHostDirectives(
673732 */
674733function parseHostDirectivesMapping (
675734 field : 'inputs' | 'outputs' , resolvedValue : ResolvedValue , classReference : ClassDeclaration ,
676- sourceExpression : ts . Expression ) : { [ publicName : string ] : string } | null {
735+ sourceExpression : ts . Expression ) : { [ bindingPropertyName : string ] : string } | null {
677736 if ( resolvedValue instanceof Map && resolvedValue . has ( field ) ) {
678737 const nameForErrors = `@Directive.hostDirectives.${ classReference . name . text } .${ field } ` ;
679738 const rawInputs = resolvedValue . get ( field ) ;
680739
681740 if ( isStringArrayOrDie ( rawInputs , nameForErrors , sourceExpression ) ) {
682- return parseInputOutputMappingArray ( rawInputs ) ;
741+ return parseMappingStringArray ( rawInputs ) ;
683742 }
684743 }
685744
0 commit comments