diff --git a/src/linker/directive_resolver.ts b/src/linker/directive_resolver.ts index f5efdef..5013c84 100644 --- a/src/linker/directive_resolver.ts +++ b/src/linker/directive_resolver.ts @@ -1,11 +1,11 @@ -import {Injectable} from '../di/decorators'; -import {Type, isPresent, isBlank, stringify} from '../facade/lang'; +import {Type, isPresent, isBlank, stringify, assign} from '../facade/lang'; import {StringMapWrapper, ListWrapper} from "../facade/collections"; - +import {reflector} from '../reflection/reflection'; import { DirectiveMetadata, ComponentMetadata, InputMetadata, + AttrMetadata, OutputMetadata, HostBindingMetadata, HostListenerMetadata @@ -16,17 +16,64 @@ import { ContentChildMetadata, ViewChildMetadata } from '../directives/metadata_di'; -import {reflector} from '../reflection/reflection'; -import {assign} from "../facade/lang"; -import {AttrMetadata} from "../directives/metadata_directives"; - +import {InjectMetadata,HostMetadata,SelfMetadata,SkipSelfMetadata,OptionalMetadata} from "../di/metadata"; type PropMetaInst = InputMetadata | OutputMetadata | HostBindingMetadata | HostListenerMetadata; +type ParamMetaInst = HostMetadata | InjectMetadata | SelfMetadata | SkipSelfMetadata; +type StringMap = {[key:string]:string}; -function _isDirectiveMetadata(type: any): boolean { +function _isDirectiveMetadata( type: any ): boolean { return type instanceof DirectiveMetadata; } +function _transformInjectedDirectivesMeta( paramsMeta: ParamMetaInst[] ): StringMap { + + if ( paramsMeta.length < 2 ) { + return; + } + + const injectInst = ListWrapper.find( paramsMeta, param=>param instanceof InjectMetadata ) as InjectMetadata; + const isHost = ListWrapper.find( paramsMeta, param=>param instanceof HostMetadata ) !== -1; + + if ( !(isHost || injectInst) ) { + return; + } + + if ( !injectInst.token ) { + throw new Error( 'no Directive instance name provided within @Inject()' ); + } + + const isOptional = ListWrapper.findIndex( paramsMeta, param=>param instanceof OptionalMetadata ) !== -1; + const isSelf = ListWrapper.findIndex( paramsMeta, param=>param instanceof SelfMetadata ) !== -1; + const isSkipSelf = ListWrapper.findIndex( paramsMeta, param=>param instanceof SkipSelfMetadata ) !== -1; + + if ( isSelf && isSkipSelf ) { + throw new Error( `you cannot provide both @Self() and @SkipSelf() for @Inject(${injectInst.token})` ); + } + + let locateType = ''; + let optionalType = isOptional + ? '?' + : ''; + if ( isHost ) { + locateType = '^'; + } + if ( isSelf ) { + locateType = ''; + } + if ( isSkipSelf ) { + locateType = '^^'; + } + + const requireExpressionPrefix = `${optionalType}${locateType}`; + const directiveName = injectInst.token; + + return { + [directiveName]: `${ requireExpressionPrefix }${ directiveName }` + }; + +} + /** * Resolve a `Type` for {@link DirectiveMetadata}. */ @@ -34,25 +81,78 @@ export class DirectiveResolver { /** * Return {@link DirectiveMetadata} for a given `Type`. */ - resolve(type: Type): DirectiveMetadata { + resolve( type: Type ): DirectiveMetadata { - const typeMetadata = reflector.annotations(type); + const metadata: DirectiveMetadata = this._getDirectiveMeta( type ); - if (isPresent(typeMetadata)) { + const propertyMetadata: {[key: string]: PropMetaInst[]} = reflector.propMetadata( type ); - const metadata: DirectiveMetadata = ListWrapper.find(typeMetadata, _isDirectiveMetadata); + return this._mergeWithPropertyMetadata( metadata, propertyMetadata ); - if (isPresent(metadata)) { + } - const propertyMetadata: {[key: string]: PropMetaInst[]} = reflector.propMetadata(type); + /** + * transform parameter annotations to required directives map so we can use it + * for DDO creation + * + * map consist of : + * - key == name of directive + * - value == Angular 1 require expression + * + * @param {Type} type + * @returns {StringMap} + */ + getRequiredDirectivesMap( type: Type ): StringMap { + + const metadata: DirectiveMetadata = this._getDirectiveMeta( type ); + + const paramMetadata = reflector.parameters( type ); + + if ( isPresent( paramMetadata ) ) { + + return paramMetadata + .reduce( ( acc, paramMetaArr )=> { + + const requireExp = _transformInjectedDirectivesMeta( paramMetaArr ); + if ( isPresent( requireExp ) ) { + assign( acc, requireExp ); + } + + return acc; + + }, {} as StringMap ); + + } - return this._mergeWithPropertyMetadata(metadata, propertyMetadata); + return {} as StringMap; + + } + + /** + * + * @param type + * @returns {DirectiveMetadata} + * @throws Error + * @private + */ + private _getDirectiveMeta( type: Type ): DirectiveMetadata { + + const typeMetadata = reflector.annotations( type ); + + if ( isPresent( typeMetadata ) ) { + + const metadata: DirectiveMetadata = ListWrapper.find( typeMetadata, _isDirectiveMetadata ); + + if ( isPresent( metadata ) ) { + + return metadata; } } - throw new Error(`No Directive annotation found on ${stringify(type)}`); + throw new Error( `No Directive annotation found on ${stringify( type )}` ); + } private _mergeWithPropertyMetadata( @@ -70,76 +170,76 @@ export class DirectiveResolver { metadata.forEach( propMetaInst => { - if (propMetaInst instanceof InputMetadata) { + if ( propMetaInst instanceof InputMetadata ) { - if (isPresent(propMetaInst.bindingPropertyName)) { - inputs.push(`${propName}: ${propMetaInst.bindingPropertyName}`); + if ( isPresent( propMetaInst.bindingPropertyName ) ) { + inputs.push( `${propName}: ${propMetaInst.bindingPropertyName}` ); } else { - inputs.push(propName); + inputs.push( propName ); } } - if (propMetaInst instanceof AttrMetadata) { + if ( propMetaInst instanceof AttrMetadata ) { - if (isPresent(propMetaInst.bindingPropertyName)) { - attrs.push(`${propName}: ${propMetaInst.bindingPropertyName}`); + if ( isPresent( propMetaInst.bindingPropertyName ) ) { + attrs.push( `${propName}: ${propMetaInst.bindingPropertyName}` ); } else { - attrs.push(propName); + attrs.push( propName ); } } - if (propMetaInst instanceof OutputMetadata) { + if ( propMetaInst instanceof OutputMetadata ) { - if (isPresent(propMetaInst.bindingPropertyName)) { - outputs.push(`${propName}: ${propMetaInst.bindingPropertyName}`); + if ( isPresent( propMetaInst.bindingPropertyName ) ) { + outputs.push( `${propName}: ${propMetaInst.bindingPropertyName}` ); } else { - outputs.push(propName); + outputs.push( propName ); } } - if (propMetaInst instanceof HostBindingMetadata) { + if ( propMetaInst instanceof HostBindingMetadata ) { - if (isPresent(propMetaInst.hostPropertyName)) { - host[`[${propMetaInst.hostPropertyName}]`] = propName; + if ( isPresent( propMetaInst.hostPropertyName ) ) { + host[ `[${propMetaInst.hostPropertyName}]` ] = propName; } else { - host[`[${propName}]`] = propName; + host[ `[${propName}]` ] = propName; } } - if (propMetaInst instanceof HostListenerMetadata) { + if ( propMetaInst instanceof HostListenerMetadata ) { const args = isPresent( propMetaInst.args ) ? propMetaInst.args.join( ', ' ) : ''; - host[`(${propMetaInst.eventName})`] = `${propName}(${args})`; + host[ `(${propMetaInst.eventName})` ] = `${propName}(${args})`; } - if (propMetaInst instanceof ContentChildrenMetadata) { - queries[propName] = propMetaInst; + if ( propMetaInst instanceof ContentChildrenMetadata ) { + queries[ propName ] = propMetaInst; } - if (propMetaInst instanceof ViewChildrenMetadata) { - queries[propName] = propMetaInst; + if ( propMetaInst instanceof ViewChildrenMetadata ) { + queries[ propName ] = propMetaInst; } - if (propMetaInst instanceof ContentChildMetadata) { - queries[propName] = propMetaInst; + if ( propMetaInst instanceof ContentChildMetadata ) { + queries[ propName ] = propMetaInst; } - if (propMetaInst instanceof ViewChildMetadata) { - queries[propName] = propMetaInst; + if ( propMetaInst instanceof ViewChildMetadata ) { + queries[ propName ] = propMetaInst; } - }); + } ); - }); + } ); - return this._merge(directiveMetadata, inputs, attrs, outputs, host, queries); + return this._merge( directiveMetadata, inputs, attrs, outputs, host, queries ); } @@ -178,7 +278,7 @@ export class DirectiveResolver { legacy: dm.legacy }; - if (dm instanceof ComponentMetadata) { + if ( dm instanceof ComponentMetadata ) { const componentSettings = assign( {}, @@ -188,11 +288,11 @@ export class DirectiveResolver { templateUrl: dm.templateUrl } ); - return new ComponentMetadata(componentSettings); + return new ComponentMetadata( componentSettings ); } else { - return new DirectiveMetadata(directiveSettings); + return new DirectiveMetadata( directiveSettings ); } diff --git a/test/linker/directive_resolver.spec.ts b/test/linker/directive_resolver.spec.ts index 3adc9d7..db1c274 100644 --- a/test/linker/directive_resolver.spec.ts +++ b/test/linker/directive_resolver.spec.ts @@ -11,169 +11,280 @@ import { Attr } from "../../src/directives/decorators"; import {DirectiveResolver} from "../../src/linker/directive_resolver"; +import {Inject} from "../../src/di/decorators"; +import {Host} from "../../src/di/decorators"; +import {Self} from "../../src/di/decorators"; +import {Optional} from "../../src/di/decorators"; +import {InjectMetadata} from "../../src/di/metadata"; +import {HostMetadata} from "../../src/di/metadata"; +import {SelfMetadata} from "../../src/di/metadata"; +import {OptionalMetadata} from "../../src/di/metadata"; +import {SkipSelf} from "../../src/di/decorators"; describe( `linker/directive_resolver`, ()=> { - it( `should return Directive metadata if exists on provided type`, ()=> { + describe( `#resolve`, ()=> { - @Directive({ - selector:'[myAttr]' - }) - class MyDirective{} + it( `should return Directive metadata if exists on provided type`, ()=> { - const resolver = new DirectiveResolver(); - const actual = resolver.resolve( MyDirective ); - const expected = true; + @Directive( { + selector: '[myAttr]' + } ) + class MyDirective { + } - expect( actual instanceof DirectiveMetadata ).to.equal( expected ); + const resolver = new DirectiveResolver(); + const actual = resolver.resolve( MyDirective ); + const expected = true; - } ); + expect( actual instanceof DirectiveMetadata ).to.equal( expected ); - it( `should return Component metadata if exists on provided type`, ()=> { + } ); - @Component({ - selector:'myComp', - template:'hello world' - }) - class MyComponent{} + it( `should return Component metadata if exists on provided type`, ()=> { - const resolver = new DirectiveResolver(); - const actual = resolver.resolve( MyComponent ); - const expected = true; + @Component( { + selector: 'myComp', + template: 'hello world' + } ) + class MyComponent { + } - expect( actual instanceof ComponentMetadata ).to.equal( expected ); + const resolver = new DirectiveResolver(); + const actual = resolver.resolve( MyComponent ); + const expected = true; - } ); + expect( actual instanceof ComponentMetadata ).to.equal( expected ); + } ); - it( `should throw error when provided type doesn't have Directive/Component metadata`, ()=> { - class NoDirective {} + it( `should throw error when provided type doesn't have Directive/Component metadata`, ()=> { - const resolver = new DirectiveResolver(); + class NoDirective {} - expect( ()=>resolver.resolve( NoDirective ) ).to.throw(`No Directive annotation found on ${stringify(NoDirective)}`); + const resolver = new DirectiveResolver(); - } ); + expect( ()=>resolver.resolve( NoDirective ) ) + .to + .throw( `No Directive annotation found on ${stringify( NoDirective )}` ); + + } ); + + it( `should update Class Metadata object accordingly with provided property Annotations`, ()=> { + + @Directive( { + selector: '[myClicker]' + } ) + class MyClicker { + + @Input() + one: string; + @Input( 'outsideAlias' ) + inside: string; + @Output() + onOne: Function; + @Output( 'onOutsideAlias' ) + onInside: Function; + + @HostBinding( 'class.disabled' ) + isDisabled: boolean; - it( `should update Class Metadata object accordingly with provided property Annotations`, ()=> { - - @Directive({ - selector:'[myClicker]' - }) - class MyClicker{ - - @Input() one: string; - @Input('outsideAlias') inside: string; - @Output() onOne: Function; - @Output('onOutsideAlias') onInside: Function; - - @HostBinding('class.disabled') isDisabled: boolean; - - @HostListener('mousemove',['$event.target']) - onMove(){} - - } - - const resolver = new DirectiveResolver(); - - const actual = resolver.resolve( MyClicker ); - const expected = new DirectiveMetadata({ - selector: '[myClicker]', - inputs: [ - 'one', - 'inside: outsideAlias' - ], - attrs: [], - outputs: [ - 'onOne', - 'onInside: onOutsideAlias' - ], - host:{ - '[class.disabled]':'isDisabled', - '(mousemove)':'onMove($event.target)', - }, - exportAs: undefined, - queries: {}, - providers: undefined - }); - - expect(actual).to.deep.equal(expected); + @HostListener( 'mousemove', [ '$event.target' ] ) + onMove() {} + + } + + const resolver = new DirectiveResolver(); + + const actual = resolver.resolve( MyClicker ); + const expected = new DirectiveMetadata( { + selector: '[myClicker]', + inputs: [ + 'one', + 'inside: outsideAlias' + ], + attrs: [], + outputs: [ + 'onOne', + 'onInside: onOutsideAlias' + ], + host: { + '[class.disabled]': 'isDisabled', + '(mousemove)': 'onMove($event.target)', + }, + exportAs: undefined, + queries: {}, + providers: undefined + } ); + + expect( actual ).to.deep.equal( expected ); + + } ); + + it( `should update merge Class Metadata object with provided property Annotations`, ()=> { + + @Component( { + selector: 'jedi', + template: '
The force is strong
', + inputs: [ 'one' ], + outputs: [ 'onOne' ], + attrs: [ 'name: publicName' ], + host: { + '[class.enabled]': 'isEnabled', + '(mouseout)': 'onMoveOut()' + }, + legacy: { + controllerAs: 'jedi' + } + } ) + class JediComponent { + + @Attr() + id: string; + @Attr( 'attrAlias' ) + noAlias: string; + + public one: string; + @Input( 'outsideAlias' ) + inside: string; + public onOne: Function; + @Output( 'onOutsideAlias' ) + onInside: Function; + + @HostBinding( 'class.disabled' ) + isDisabled: boolean; + + private get isEnabled() { return true }; + + @HostListener( 'mousemove', [ '$event.target' ] ) + onMove() {} + + onMoveOut() {} + + } + + const resolver = new DirectiveResolver(); + + const actual = resolver.resolve( JediComponent ); + const expected = new ComponentMetadata( { + selector: 'jedi', + template: '
The force is strong
', + inputs: [ + 'one', + 'inside: outsideAlias' + ], + attrs: [ + 'name: publicName', + 'id', + 'noAlias: attrAlias' + ], + outputs: [ + 'onOne', + 'onInside: onOutsideAlias' + ], + host: { + '[class.disabled]': 'isDisabled', + '[class.enabled]': 'isEnabled', + '(mousemove)': 'onMove($event.target)', + '(mouseout)': 'onMoveOut()' + }, + exportAs: undefined, + queries: {}, + providers: undefined, + legacy: { + controllerAs: 'jedi' + } + } ); + + expect( actual ).to.deep.equal( expected ); + + } ); } ); - it( `should update merge Class Metadata object with provided property Annotations`, ()=> { - - @Component({ - selector:'jedi', - template:'
The force is strong
', - inputs:['one'], - outputs:['onOne'], - attrs:['name: publicName'], - host:{ - '[class.enabled]':'isEnabled', - '(mouseout)':'onMoveOut()' - }, - legacy:{ - controllerAs:'jedi' + describe( `#getDirectivesToInject`, ()=> { + + it( `should return require expression map from param metadata which are injecting directives`, ()=> { + + @Directive({selector:'[foo]'}) + class Foo{ + + constructor( + @Inject( '$log' ) private $log, + @Inject( '$attrs' ) private $attrs, + @Inject('clicker') @Host() private clicker, + @Inject('api') @Host() @Optional() private api, + @Inject('ngModel') @Host() @Self() private ngModel + ) {} + } - }) - class JediComponent{ - - @Attr() id: string; - @Attr('attrAlias') noAlias: string; - - public one: string; - @Input('outsideAlias') inside: string; - public onOne: Function; - @Output('onOutsideAlias') onInside: Function; - - @HostBinding('class.disabled') isDisabled: boolean; - private get isEnabled(){ return true }; - - @HostListener('mousemove',['$event.target']) - onMove(){} - - onMoveOut(){} - - } - - const resolver = new DirectiveResolver(); - - const actual = resolver.resolve( JediComponent ); - const expected = new ComponentMetadata({ - selector: 'jedi', - template:'
The force is strong
', - inputs: [ - 'one', - 'inside: outsideAlias' - ], - attrs:[ - 'name: publicName', - 'id', - 'noAlias: attrAlias' - ], - outputs: [ - 'onOne', - 'onInside: onOutsideAlias' - ], - host:{ - '[class.disabled]':'isDisabled', - '[class.enabled]': 'isEnabled', - '(mousemove)':'onMove($event.target)', - '(mouseout)': 'onMoveOut()' - }, - exportAs: undefined, - queries: {}, - providers: undefined, - legacy:{ - controllerAs:'jedi' + + const resolver = new DirectiveResolver(); + const actual = resolver.getRequiredDirectivesMap(Foo); + const expected = { + clicker: '^clicker', + api: '?^api', + ngModel: 'ngModel' + }; + + expect(actual).to.deep.equal(expected); + + } ); + + it( `should return empty StringMap when no directive injected`, ()=> { + + @Directive({selector:'[foo]'}) + class Foo{ + constructor( + @Inject('$log') private $log + ){} + } + + const resolver = new DirectiveResolver(); + const actual = resolver.getRequiredDirectivesMap(Foo); + const expected = {}; + + expect(actual).to.deep.equal(expected); + + } ); + + it( `should throw if Inject is missing inject token`, ()=> { + + @Directive({selector:'[foo]'}) + class Foo{ + constructor( + @Inject('') @Host() private ngModel + ){} + } + + const resolver = new DirectiveResolver(); + + expect(()=>resolver.getRequiredDirectivesMap(Foo)).to.throw( + `no Directive instance name provided within @Inject()` + ); + + } ); + + it( `should throw if both @Self and @SkipSelf are used`, ()=> { + + @Directive({selector:'[foo]'}) + class Foo{ + constructor( + @Inject('ngModel') @Host() @SkipSelf() @Self() private ngModel + ){} } - }); - expect(actual).to.deep.equal(expected); + const resolver = new DirectiveResolver(); + + expect(()=>resolver.getRequiredDirectivesMap(Foo)).to.throw( + `you cannot provide both @Self() and @SkipSelf() for @Inject(ngModel)` + ); + + } ); } ); + } );