diff --git a/package.json b/package.json index a2f97fe..2d3f93a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "homepage": "https://github.com/ngParty/ng-metadata#readme", "devDependencies": { + "@angular/core": "2.4.1", "@types/angular": "1.5.19", "@types/chai": "3.4.34", "@types/jquery": "2.0.34", @@ -47,15 +48,16 @@ "live-server": "0.8.2", "mocha": "3.1.2", "reflect-metadata": "0.1.8", - "rxjs": "5.0.0-rc.1", + "rxjs": "5.0.1", "sinon": "1.17.6", "systemjs": "0.19.6", "ts-node": "1.7.0", - "typescript": "2.2.0-dev.20161115", - "validate-commit-msg": "2.0.0" + "typescript": "2.1.4", + "validate-commit-msg": "2.0.0", + "zone.js": "^0.7.2" }, "peerDependencies": { - "rxjs": "5.0.0-rc.1", + "rxjs": "^5.0.1", "angular": ">=1.4.x" }, "dependencies": {}, diff --git a/src/upgrade/static/downgrade_component.ts b/src/upgrade/static/downgrade_component.ts new file mode 100644 index 0000000..78cbbfa --- /dev/null +++ b/src/upgrade/static/downgrade_component.ts @@ -0,0 +1,136 @@ +import { Type } from '../../facade/type'; +import { reflector } from '../../core/reflection/reflection'; +import { resolveDirectiveNameFromSelector, isString } from '../../facade/lang'; +import { StringMapWrapper } from '../../facade/collections'; + +export type ProvideNg2ComponentParams = { + component:Type, + downgradeFn:downgradeComponent +}; +export type downgradeComponent = (info: { + component: Type, + inputs?: string[], + outputs?: string[], +}) => any; + + +/** + * Used to register an Angular 2 Component as a directive on an Angular 1 module, + * where the directive name and bindings(inputs,outputs) are automatically created from the selector. + * + * @example + * ```typescript + * // app.module.ts + * import * as angular from 'angular' + * import { downgradeComponent } from '@angular/upgrade/static/'; + * import { downgradeNg2Component } from 'ng-metadata/upgrade'; + * import { provide } from 'ng-metadata/core'; + * + * export const AppModule = angular + * .module('myApp',[]) + * .directive(...downgradeNg2Component({component:Ng2Component,downgradeFn:downgradeComponent})) + * ``` + */ +export function downgradeNg2Component({component,downgradeFn}: ProvideNg2ComponentParams): [string,Function] { + const {name,factoryFn} = _downgradeComponent({component,downgradeFn}); + return [name,factoryFn] +} + +/** + * Used to register an Angular 2 Component by including it in the `declarations` array of an ng-metadata `@NgModule`, + * where the directive name and bindings(inputs,outputs) are automatically created from the selector. + * + * @example + * ```typescript + * // app.module.ts + * import { downgradeComponent } from '@angular/upgrade/static/'; + * import { provideNg2Component } from 'ng-metadata/upgrade'; + * import { NgModule } from 'ng-metadata/core'; + * + * @NgModule({ + * declarations:[ + * provideNg2Component({component:Ng2Component,downgradeFn:downgradeComponent}) + * ] + * }) + * export class AppModule {}; + * ``` + */ +export function provideNg2Component({component,downgradeFn}: ProvideNg2ComponentParams): Function { + const {name,factoryFn} = _downgradeComponent({component,downgradeFn}); + + reflector.registerDowngradedNg2ComponentName(name, factoryFn); + return factoryFn; +} + +/** + * + * @private + * @internal + */ +export function _downgradeComponent({component,downgradeFn}: ProvideNg2ComponentParams): {name:string, factoryFn:Function} { + // process inputs,outputs + const propAnnotations = reflector.propMetadata(component); + const {inputs=[],outputs=[]} = _getOnlyInputOutputMetadata(propAnnotations) || {}; + + // process @Component + const annotations = reflector.annotations(component); + const cmpAnnotation = annotations[0]; + const directiveName = resolveDirectiveNameFromSelector(cmpAnnotation.selector); + + const downgradedDirectiveFactory = downgradeFn( + StringMapWrapper.assign( + {}, + inputs.length ? {inputs: inputs} : {}, + outputs.length ? {outputs: outputs} : {}, + {component: component}, + ) + ); + return { + name: directiveName, + factoryFn: downgradedDirectiveFactory + }; +} + +type Ng2InputOutputPropDecoratorFactory = [{ + bindingPropertyName?: string, + toString(): string +}] +function _getOnlyInputOutputMetadata(metadata:{[propName:string]:Ng2InputOutputPropDecoratorFactory[]}){ + if(StringMapWrapper.isEmpty( metadata)){ + return; + } + const inputOutput = { + inputs: [], + outputs: [] + }; + StringMapWrapper.forEach(metadata, (metaItem: Ng2InputOutputPropDecoratorFactory, key: string) => { + if(_isNg2InputPropDecoratorFactory(metaItem)){ + inputOutput.inputs.push( + _createBindingFromNg2PropDecoratorFactory(key,metaItem[0].bindingPropertyName) + ); + return; + } + if(_isNg2OutputPropDecoratorFactory(metaItem)){ + inputOutput.outputs.push( + _createBindingFromNg2PropDecoratorFactory(key,metaItem[0].bindingPropertyName) + ); + return; + } + }); + return inputOutput; +} +function _createBindingFromNg2PropDecoratorFactory(prop:string,attr?:string): string { + return isString(attr) ? `${prop}: ${attr}` : `${prop}`; +} +function _isNg2InputPropDecoratorFactory(metadataValues:any[]): boolean{ + return _isNg2InputOutputPropDecoratorFactory(metadataValues,'@Input'); +} +function _isNg2OutputPropDecoratorFactory(metadataValues:any[]): boolean{ + return _isNg2InputOutputPropDecoratorFactory(metadataValues,'@Output'); +} +function _isNg2InputOutputPropDecoratorFactory(metadataValues:any[], type:'@Input'|'@Output'): boolean { + return metadataValues.some((metaValue:any)=>{ + const decoratorType = metaValue.toString(); + return decoratorType === type; + }) +} diff --git a/src/upgrade/static/downgrade_injectable.ts b/src/upgrade/static/downgrade_injectable.ts new file mode 100644 index 0000000..c8b13d3 --- /dev/null +++ b/src/upgrade/static/downgrade_injectable.ts @@ -0,0 +1,157 @@ +import { OpaqueToken } from '../../core/di/opaque_token'; +import { getInjectableName } from '../../core/di/provider'; +import { ProviderLiteral } from '../../core/di/provider_util'; +import { Type } from '../../facade/type'; + +export type ProvideNg2InjectableParams = { + injectable: Function | Type, + downgradeFn: Function, + /** + * We need token only if downgraded ANgular 2 Service is not Decorated with both ng2 @Injectable and ngMetadata @Injectable + * + */ + token?: string | OpaqueToken, +} + +/** + * Downgrades an Angular 2 Injectable so that it can be registered as an Angular 1 + * factory. Either a string or an ng-metadata OpaqueToken can be used for the name. + * + * **NOTE:** downgraded service must also be registered within Angular 2 Component or NgModule + * + * @example + * ```typescript + * // app.module.ts + * import * as angular from 'angular' + * import { downgradeInjectable } from '@angular/upgrade/static/'; + * import { downgradeNg2Injectable } from 'ng-metadata/upgrade'; + * import { provide } from 'ng-metadata/core'; + * + * import { Ng2Service } from './services/ng2.service; + * import { Ng2ServiceDecorated } from './services/ng2decorated.service + * + * export const OtherServiceToken = new OpaqueToken('otherService') + * + * export const AppModule = angular + * .module('myApp',[]) + * .factory(...downgradeNg2Injectable({token:'ng2Service', injectable: Ng2Service, downgradeFn: downgradeInjectable })) + * .factory(...downgradeNg2Injectable({token: OtherServiceToken, injectable: Ng2Service, downgradeFn: downgradeInjectable })) + * .factory(...downgradeNg2Injectable({injectable:Ng2ServiceDecorated, downgradeFn: downgradeInjectable})) + * ``` + */ +export function downgradeNg2Injectable( { injectable, downgradeFn, token }: ProvideNg2InjectableParams ): [string|Function] { + const { name, factoryFn } = _downgradeInjectable( { + token: token || injectable as any, + injectable, + downgradeFn + } ); + return [ name, factoryFn ] +} + + +/** + * Returns a ProviderLiteral which can be used to register an Angular 2 Provider/Injectable + * by including it in the providers array of an ng-metadata annotated Angular 1 + * Component or NgModule. Either a string or an ng-metadata OpaqueToken can be used for the name. + * + * **NOTE:** downgraded service must also be registered within Angular 2 Component or NgModule + * + * @example + * ``` + * // foo.component.ts - Angular 1(ngMetadata) + * import { downgradeInjectable } from '@angular/upgrade/static/'; + * import { provideNg2Injectable } from 'ng-metadata/upgrade'; + * import { Component } from 'ng-metadata/core'; + * + * import { Ng2Service } from './services/ng2.service; + * import { Ng2ServiceDecorated } from './services/ng2decorated.service; + * + * const OtherServiceToken = new OpaqueToken('otherService') + * + * @Component({ + * selector: 'my-foo', + * providers: [ + * provideNg2Injectable({token:'ng2Service', injectable: Ng2Service, downgradeFn: downgradeInjectable }), + * provideNg2Injectable({token:OtherServiceToken, injectable: Ng2Service, downgradeFn: downgradeInjectable }), + * provideNg2Injectable({injectable:Ng2ServiceDecorated, downgradeFn: downgradeInjectable}), + * ], + * }) + * class FooComponent{} + * ``` + * + * or via ngMetadata NgModule: + * + * @example + * ```typescript + * * @example + * ``` + * // app.module.ts - Angular 1(ngMetadata) + * import { downgradeInjectable } from '@angular/upgrade/static/'; + * import { provideNg2Injectable } from 'ng-metadata/upgrade'; + * import { NgModule } from 'ng-metadata/core'; + * + * import { Ng2Service } from './services/ng2.service; + * import { Ng2ServiceDecorated } from './services/ng2decorated.service + * + * const OtherServiceToken = new OpaqueToken('otherService') + * + * @NgModule({ + * providers: [ + * provideNg2Injectable({token:'ng2Service', injectable: Ng2Service, downgradeFn: downgradeInjectable }), + * provideNg2Injectable({token:OtherServiceToken, injectable: Ng2Service, downgradeFn: downgradeInjectable }), + * provideNg2Injectable({injectable:Ng2ServiceDecorated, downgradeFn: downgradeInjectable}), + * ], + * }) + * export class AppModule{} + * ``` + * + * as you've may noticed in one registration we've omitted `token`, how is that possible that it works you ask? + * this is thanks to ngMetadata `@Injectable()` decorator, we can decorate Angular 2 Classes with our ngMetadata `@Injectable`, + * which gives us benefit to omit Opaque tokens creation and use the same class for DI for both Angular 2 and Angular 1. + * POWER OVERWHELMING RIGHT?! + * + * Enough Talk! Show me how the service looks like: + * ```typescript + * // ./services/ng2decorated.service.ts + * + * import {Injectable} from '@angular/core'; + * import {Injectable as KeepNg1Injectable} from 'ng-metadata/core'; + * + * @KeepNg1Injectable() + * @Injectable() + * export class Ng2ServiceDecorated { + * constructor(){} + * greet(){} + * } + * ``` + */ +export function provideNg2Injectable( { injectable, downgradeFn, token }: ProvideNg2InjectableParams ): ProviderLiteral { + const { name, factoryFn, deps } = _downgradeInjectable( { + token: token || injectable as any, + injectable, + downgradeFn + } ); + + return { + provide: name, + useFactory: factoryFn, + deps: deps, + }; +} + +/** + * + * @private + * @internal + */ +export function _downgradeInjectable( { token, injectable, downgradeFn }: ProvideNg2InjectableParams ): {name: string, factoryFn: Function, deps: string[]} { + const downgradedInjectableFactory = downgradeFn( injectable ); + const { $inject = [] } = downgradedInjectableFactory; + const name = getInjectableName( token ); + + return { + name, + factoryFn: downgradedInjectableFactory, + deps: $inject + }; +} diff --git a/src/upgrade/static/static.ts b/src/upgrade/static/static.ts new file mode 100644 index 0000000..4e4d498 --- /dev/null +++ b/src/upgrade/static/static.ts @@ -0,0 +1,3 @@ +export { provideNg2Component, downgradeNg2Component } from './downgrade_component'; +export { provideNg2Injectable, downgradeNg2Injectable } from './downgrade_injectable'; +export { upgradeInjectable } from './upgrade_injectable'; diff --git a/src/upgrade/static/upgrade_injectable.ts b/src/upgrade/static/upgrade_injectable.ts new file mode 100644 index 0000000..b0557ca --- /dev/null +++ b/src/upgrade/static/upgrade_injectable.ts @@ -0,0 +1,137 @@ +import { OpaqueToken } from '../../core/di/opaque_token'; +import { Type } from '../../facade/type'; +import { getInjectableName } from '../../core/di/provider'; + +/** + * Let's say we have ngMetadata angular 1 Service: + * + * ```typescript + * // heroes.service.ts + * import { Injectable } from 'ng-metadata/core' + * import { Hero } from './hero'; + * + * @Injectable() + * export class HeroesService { + * get() { + * return [ + * new Hero(1, 'Windstorm'), + * new Hero(2, 'Spiderman'), + * ]; + * } + * } + * ``` + * + * registered within ng-metadata NgModule: + * + * ```typescript + * // app.module.ts + * import { NgModule } from 'ng-metadata/core'; + * import { HeroesService } from './heroes/heroes.service'; + * + * @NgModule( { + * providers: [ HeroesService ] + * } ) + * class AppModule {} + * ``` + * + * and we can upgrade it to Angular 2 like this: + * + * ```typescript + * // app.module.ng2.ts + * import { NgModule } from '@angular/core'; + * import { BrowserModule } from '@angular/platform-browser'; + * import { UpgradeModule } from '@angular/upgrade/static/'; + * import { provideNg1Injectable } from 'ng-metadata/upgrade'; + * + * import { HeroComponent } from './heroes/hero.component.ng2'; + * import { HeroesService } from './heroes/heroes.service'; + * + * @NgModule({ + * imports: [ + * BrowserModule, + * UpgradeModule + * ], + * declarations: [ + * HeroComponent + * ], + * providers: [ + * provideNg1Injectable('$routeParams'), + * provideNg1Injectable(HeroesService), + * ], + * entryComponents: [ + * HeroComponent + * ] + * }) + * export class AppModule { + * // preventing automatic Bootstrap + * ngDoBootstrap() {} + * } + * ``` + * + * and now we can use it within angular 2 Component: + * ```typescript + * // hero.component.ng2.ts + * import { Component } from '@angular/core'; + * + * @Component({ + * selector: 'my-hero', + * template: `

My Hero

`, + * }) + * class HeroComponent { + * constructor( + * @Inject('$routeParams') private $routeParams: any, // by name using @Inject + * private myCoolSvc: MyCoolService // by type using the user defined token + * private heroesSvc: HeroesService // by type using ngMetadata @Injectable service class + * ) {} + * } + * ``` + * + * `provideNg1Injectable` accept also as main argument: + * - string(injected via @Inject('myString')) + * - OpaqueToken (injected via @Inject(myToken)) + * + * secondary(optional) argument `asToken` can be used when: + * - if you have ng1 service not registered with ng-metadata but it's a class and you wanna use this class for DI in ng2, + * then you have to do it like this: + * + * ```typescript + * import * as angular from 'angular'; + * + * class MyCoolService {} + * + * angular.module('myApp',[]) + * .service('myCoolSvc',MyCoolService); + * + * @NgModule({ + * imports: [ + * BrowserModule, + * UpgradeModule + * ], + * declarations: [ + * HeroComponent + * ], + * providers: [ + * provideNg1Injectable('myCoolSvc',MyCoolService) + * ], + * entryComponents: [ + * HeroComponent + * ] + * }) + * export class AppModule { + * // preventig automatic Bootstrap + * ngDoBootstrap() {} + * } + * ``` + */ +export function upgradeInjectable( + injectable: string|OpaqueToken|Function|Type, + asToken: string|OpaqueToken|Function|Type = injectable +) { + return { + // this can be any token, but because we are using ng-metadata we are already using TS DI via type + // allow to provide custom Token ( for instance if user want's just provide es6 class and service is registered via string in ng1 ) + provide: asToken, + useFactory: ( $injector: ng.auto.IInjectorService ) => $injector.get( getInjectableName( injectable ) ), + deps: [ '$injector' ] + } +} diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index 5433c5e..68863b4 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -1 +1,2 @@ export { NgMetadataUpgradeAdapter } from './upgrade_adapter'; +export * from './static/static'; diff --git a/src/upgrade/upgrade_adapter.ts b/src/upgrade/upgrade_adapter.ts index fe7730b..f0fd519 100644 --- a/src/upgrade/upgrade_adapter.ts +++ b/src/upgrade/upgrade_adapter.ts @@ -131,11 +131,32 @@ export class NgMetadataUpgradeAdapter { * When using the upgraded Provider for DI, either the string name can be used with @Inject, or * a given token can be injected by type. * - * E.g. - * class $state {} + * @example + * ```typescript + * import {Injectable, NgModule} from 'ng-metadata/core'; + * import * as angular from 'angular'; + * + * class MyCoolService {} + * + * angular.module('myApp',[]) + * .service('myCoolSvc',MyCoolService) + * + * @Injectable() + * class MyService{} * - * upgradeAdapter.upgradeNg1Provider('$state', { asToken: $state }) - * upgradeAdapter.upgradeNg1Provider('$rootScope') + * @NgModule({ + * providers: [MyService] + * }) + * class AppModule{} + * + * // upgrade.module.ts + * upgradeAdapter.upgradeNg1Provider(MyService) + * upgradeAdapter.upgradeNg1Provider('myCoolSvc', { asToken: MyCoolService }) + * // angular 1 core services + * upgradeAdapter.upgradeNg1Provider('$routeParams') + * + * // angular 2 Component + * import { Component } from '@angular/core'; * * @Component({ * selector: 'ng2', @@ -143,14 +164,18 @@ export class NgMetadataUpgradeAdapter { * }) * class Ng2Component { * constructor( - * @Inject('$rootScope') private $rootScope: any, // by name using @Inject - * private $state: $state // by type using the user defined token + * @Inject('$routeParams') private $routeParams: any, // by name using @Inject + * private myCoolSvc: MyCoolService // by type using the user defined token + * private mySvc: MyService // by type using ngMetadata @Injectable service class * ) {} * } - * + *``` */ - upgradeNg1Provider( name: string, options?: { asToken: any; } ): void { - return this._upgradeAdapter.upgradeNg1Provider( name, options ); + upgradeNg1Provider( + name: string|OpaqueToken|Type, + options: { asToken: string|OpaqueToken|Type|Function } = { asToken: name } + ) { + return this._upgradeAdapter.upgradeNg1Provider( getInjectableName(name), options ); } } diff --git a/test/index.ts b/test/index.ts index 894bf7f..c77fca3 100644 --- a/test/index.ts +++ b/test/index.ts @@ -24,12 +24,17 @@ import './core/directives/controller/controller_factory.spec'; import './core/directives/host/host_parser.spec'; import './core/directives/host/host_resolver.spec'; import './core/directives/query/children_resolver.spec'; + import './facade/lang.spec'; import './facade/primitives.spec'; import './facade/collections.spec'; import './facade/async.spec'; + import './common/pipes/async_pipe.spec'; + import './upgrade/upgrade_adapter.spec'; +import './upgrade/static/upgrade_injectable.spec'; +import './upgrade/static/downgrade_injectable.spec'; +import './upgrade/static/downgrade_component.spec'; -describe( 'ng-metadata', ()=> { -} ); +describe( 'ng-metadata', ()=> {} ); diff --git a/test/upgrade/static/downgrade_component.spec.ts b/test/upgrade/static/downgrade_component.spec.ts new file mode 100644 index 0000000..496c253 --- /dev/null +++ b/test/upgrade/static/downgrade_component.spec.ts @@ -0,0 +1,77 @@ +import * as sinon from 'sinon'; +import { expect } from 'chai'; + +import { isFunction } from '../../../src/facade/lang'; +import { + downgradeNg2Component, + provideNg2Component, + _downgradeComponent +} from '../../../src/upgrade/static/downgrade_component'; +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +describe( `downgrade_component`, () => { + describe( `downgradeNg2Component`, () => { + it( `should be a function`, () => { + expect( isFunction( downgradeNg2Component ) ).to.equal( true ); + } ); + } ); + describe( `provideNg2Component`, () => { + it( `should be a function`, () => { + expect( isFunction( provideNg2Component ) ).to.equal( true ); + } ); + } ); + describe( `_downgradeComponent`, () => { + + let downgradeFnStub: Sinon.SinonStub; + const noop = () => {}; + + @Component({ + selector:'with-inout', + template: '', + }) + class WithInOutComponent{ + @Input() name: string; + @Output() call = new EventEmitter(); + } + + @Component({ + selector: 'just-cmp', + template: '', + }) + class JustCmpComponent{} + + beforeEach( () => { + downgradeFnStub = sinon.stub(); + downgradeFnStub.returns(noop) + } ); + it( `should downgrade ng2Component to ng1 directive name and directiveFactory`, () => { + const downgradedResult = _downgradeComponent( { component: JustCmpComponent, downgradeFn: downgradeFnStub } ); + + expect( downgradeFnStub.called ).to.equal( true ); + expect( downgradeFnStub.calledWith( { + component: JustCmpComponent, + } ) ).to.equal( true ); + expect( downgradedResult ).to.deep.equal( { + name: 'justCmp', + factoryFn: noop + } ) + + } ); + it( `should automatically extract inputs and outputs from ng2Components @Input,@Output`, () => { + + const downgradedResult = _downgradeComponent( { component: WithInOutComponent, downgradeFn: downgradeFnStub } ); + + expect( downgradeFnStub.called ).to.equal( true ); + expect( downgradeFnStub.calledWith( { + inputs: [ 'name' ], + outputs: [ 'call' ], + component: WithInOutComponent, + } ) ).to.equal( true ); + expect( downgradedResult ).to.deep.equal( { + name: 'withInout', + factoryFn: noop + } ) + + } ); + } ); +} ); diff --git a/test/upgrade/static/downgrade_injectable.spec.ts b/test/upgrade/static/downgrade_injectable.spec.ts new file mode 100644 index 0000000..01f7598 --- /dev/null +++ b/test/upgrade/static/downgrade_injectable.spec.ts @@ -0,0 +1,84 @@ +import { expect } from 'chai'; +import { isFunction } from '../../../src/facade/lang'; +import { + downgradeNg2Injectable, + provideNg2Injectable, + _downgradeInjectable +} from '../../../src/upgrade/static/downgrade_injectable'; +import { Injectable } from '../../../src/core/di/decorators'; +import { OpaqueToken } from '../../../src/core/di/opaque_token'; +import { globalKeyRegistry } from '../../../src/core/di/key'; + +describe( `downgrade_injectable`, () => { + + function dummyNg2DowngradedResult(){} + function downgradeFn(injectable:any){return dummyNg2DowngradedResult} + + class Ng2Injectable{} + + beforeEach( () => { + globalKeyRegistry._reset(); + } ); + + describe( `downgradeNg2Injectable`, () => { + it( `should be a function`, () => { + expect(isFunction(downgradeNg2Injectable)).to.equal(true); + } ); + } ); + describe( `provideNg2Injectable`, () => { + it( `should be a function`, () => { + expect(isFunction(provideNg2Injectable)).to.equal(true); + } ); + } ); + describe( `_downgradeInjectable`, () => { + + it( `should return ProviderType for ngMetadata factory registration`, () => { + + const provider = _downgradeInjectable( { + token: 'foo', + injectable: Ng2Injectable, + downgradeFn: downgradeFn + } ); + + expect( provider.name ).to.equal( 'foo' ); + expect( provider.factoryFn ).to.equal( dummyNg2DowngradedResult ); + expect( provider.deps ).to.deep.equal( [] ); + + } ); + + describe( `token supports`, () => { + + it( `should support string`, () => { + const provider = _downgradeInjectable( { + token: 'foo', + injectable: Ng2Injectable, + downgradeFn: downgradeFn + } ); + + expect(provider.name).to.equal('foo'); + } ); + it( `should support ngMetadata OpaqueToken`, () => { + const provider = _downgradeInjectable( { + token: new OpaqueToken('boom'), + injectable: Ng2Injectable, + downgradeFn: downgradeFn + } ); + + expect(provider.name).to.equal('boom'); + } ); + it( `should support ng2Injectable decorated with ngMetadata Injectable`, () => { + @Injectable() + class NgMetadataInjectable{} + + const provider = _downgradeInjectable( { + token: NgMetadataInjectable as any, + injectable: Ng2Injectable, + downgradeFn: downgradeFn + } ); + + expect(provider.name).to.equal('ngMetadataInjectable#1'); + } ); + } ); + + } ); +} ); diff --git a/test/upgrade/static/upgrade_injectable.spec.ts b/test/upgrade/static/upgrade_injectable.spec.ts new file mode 100644 index 0000000..3f0a85b --- /dev/null +++ b/test/upgrade/static/upgrade_injectable.spec.ts @@ -0,0 +1,80 @@ +import { expect } from 'chai'; +import { upgradeInjectable } from '../../../src/upgrade/static/upgrade_injectable'; +import { isFunction } from '../../../src/facade/lang'; +import { OpaqueToken } from '../../../src/core/di/opaque_token'; +import { Injectable } from '../../../src/core/di/decorators'; +import { globalKeyRegistry } from '../../../src/core/di/key'; + +describe(`upgrade_injectable`, () => { + + it(`should be a function`, () => { + expect(isFunction(upgradeInjectable)).to.equal(true) + }); + + it(`should return ng2 factory provider`, () => { + const actual = upgradeInjectable('helloSvc'); + const expected = { + provide: 'helloSvc', + useFactory: ()=>({}), + deps: ['$injector'] + }; + + expect(Object.keys(actual)).to.deep.equal(['provide','useFactory','deps']); + expect(actual.provide).to.equal('helloSvc'); + expect(isFunction(actual.useFactory)).to.equal(true); + expect(actual.deps).to.deep.equal(['$injector']); + }); + + describe(`injectable/asToken supports`, () => { + + const $injector: any = { + get(injectableName:string){ + return injectableName + } + }; + function getInjectable(ng2ProviderDefinition:any){ + return ng2ProviderDefinition.useFactory($injector) + } + + beforeEach( () => { + globalKeyRegistry._reset(); + } ); + + it( `should support injectable as string`, () => { + const provider = upgradeInjectable('helloSvc'); + const actual = getInjectable(provider); + + expect( actual ).to.equal( 'helloSvc' ); + } ); + it( `should support injectable as OpaqueToken`, () => { + const injectable = new OpaqueToken( 'boom' ); + const provider = upgradeInjectable( injectable ); + + const actual = getInjectable(provider); + expect( actual ).to.equal( 'boom' ); + } ); + it( `should support injectable as decorated Type`, () => { + @Injectable() + class HelloService {} + const provider = upgradeInjectable(HelloService); + + const actual = getInjectable(provider); + expect( actual ).to.equal( 'helloService#1' ); + } ); + it( `should throw if user provides undecorated Type as injectable`, () => { + class HelloService {} + const provider = upgradeInjectable(HelloService); + + const actual = ()=>getInjectable(provider); + expect(actual).to.throw; + } ); + it( `should support optional asToken if we wanna register undecorated type to ng2`, () => { + class NonNgMetadataService {} + const provider = upgradeInjectable('helloSvc',NonNgMetadataService); + + expect( provider.provide ).to.equal( NonNgMetadataService ); + expect( getInjectable(provider) ).to.equal( 'helloSvc' ); + } ); + + }); +}); diff --git a/upgrade.ts b/upgrade.ts index e64d178..6584539 100644 --- a/upgrade.ts +++ b/upgrade.ts @@ -1 +1 @@ -export { NgMetadataUpgradeAdapter } from './src/upgrade/upgrade_adapter'; +export * from './src/upgrade/upgrade'