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: `