From d896e4350a950c758dbb19cb71dd928835ec84c1 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 15 Oct 2015 12:04:19 -0700 Subject: [PATCH] feat(ngUpgrade): add support for upgrade/downgrade of injectables Closes #4766 --- modules/upgrade/src/upgrade_adapter.ts | 134 +++++++++++++++++++++-- modules/upgrade/test/integration_spec.ts | 36 +++++- 2 files changed, 161 insertions(+), 9 deletions(-) diff --git a/modules/upgrade/src/upgrade_adapter.ts b/modules/upgrade/src/upgrade_adapter.ts index 63032d8c19..840a822b4d 100644 --- a/modules/upgrade/src/upgrade_adapter.ts +++ b/modules/upgrade/src/upgrade_adapter.ts @@ -7,10 +7,12 @@ import { ApplicationRef, AppViewManager, Compiler, + Inject, Injector, NgZone, PlatformRef, ProtoViewRef, + Provider, Type } from 'angular2/angular2'; import {applicationDomBindings} from 'angular2/src/core/application_common'; @@ -113,6 +115,8 @@ export class UpgradeAdapter { private upgradedComponents: Type[] = []; /* @internal */ private downgradedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder} = {}; + /* @internal */ + private providers: Array = []; /** * Allows Angular v2 Component to be used from AngularJS v1. @@ -298,7 +302,8 @@ export class UpgradeAdapter { applicationDomBindings(), compilerProviders(), provide(NG1_INJECTOR, {useFactory: () => ng1Injector}), - provide(NG1_COMPILE, {useFactory: () => ng1Injector.get(NG1_COMPILE)}) + provide(NG1_COMPILE, {useFactory: () => ng1Injector.get(NG1_COMPILE)}), + this.providers ]); var injector: Injector = applicationRef.injector; var ngZone: NgZone = injector.get(NgZone); @@ -349,16 +354,125 @@ export class UpgradeAdapter { Promise.all([this.compileNg2Components(compiler, protoViewRefMap), ng1compilePromise]) .then(() => { ngZone.run(() => { - rootScopePrototype.$apply = original$applyFn; // restore original $apply - while (delayApplyExps.length) { - rootScope.$apply(delayApplyExps.shift()); + if (rootScopePrototype) { + rootScopePrototype.$apply = original$applyFn; // restore original $apply + while (delayApplyExps.length) { + rootScope.$apply(delayApplyExps.shift()); + } + (upgrade)._bootstrapDone(applicationRef, ng1Injector); + rootScopePrototype = null; } - (upgrade)._bootstrapDone(applicationRef, ng1Injector); }); }, onError); return upgrade; } + /** + * Adds a provider to the top level environment of a hybrid AngularJS v1 / Angular v2 application. + * + * In hybrid AngularJS v1 / Angular v2 application, there is no one root Angular v2 component, + * for this reason we provide an application global way of registering providers which is + * consistent with single global injection in AngularJS v1. + * + * ## Example + * + * ``` + * class Greeter { + * greet(name) { + * alert('Hello ' + name + '!'); + * } + * } + * + * @Component({ + * selector: 'app', + * template: '' + * }) + * class App { + * constructor(greeter: Greeter) { + * this.greeter('World'); + * } + * } + * + * var adapter = new UpgradeAdapter(); + * adapter.addProvider(Greeter); + * + * var module = angular.module('myExample', []); + * module.directive('app', adapter.downgradeNg2Component(App)); + * + * document.body.innerHTML = '' + * adapter.bootstrap(document.body, ['myExample']); + *``` + */ + public addProvider(provider: Type | Provider | any[]): void { this.providers.push(provider); } + + /** + * Allows AngularJS v1 service to be accessible from Angular v2. + * + * + * ## Example + * + * ``` + * class Login { ... } + * class Server { ... } + * + * @Injectable() + * class Example { + * constructor(@Inject('server') server, login: Login) { + * ... + * } + * } + * + * var module = angular.module('myExample', []); + * module.service('server', Server); + * module.service('login', Login); + * + * var adapter = new UpgradeAdapter(); + * adapter.upgradeNg1Provider('server'); + * adapter.upgradeNg1Provider('login', {asToken: Login}); + * adapter.addProvider(Example); + * + * adapter.bootstrap(document.body, ['myExample']).ready((ref) => { + * var example: Example = ref.ng2Injector.get(Example); + * }); + * + * ``` + */ + public upgradeNg1Provider(name: string, options?: {asToken: any}) { + var token = options && options.asToken || name; + this.providers.push(provide(token, { + useFactory: (ng1Injector: angular.auto.IInjectorService) => ng1Injector.get(name), + deps: [NG1_INJECTOR] + })); + } + + /** + * Allows Angular v2 service to be accessible from AngularJS v1. + * + * + * ## Example + * + * ``` + * class Example { + * } + * + * var adapter = new UpgradeAdapter(); + * adapter.addProvider(Example); + * + * var module = angular.module('myExample', []); + * module.factory('example', adapter.downgradeNg2Provider(Example)); + * + * adapter.bootstrap(document.body, ['myExample']).ready((ref) => { + * var example: Example = ref.ng1Injector.get('example'); + * }); + * + * ``` + */ + public downgradeNg2Provider(token: any): Function { + var factory = function(injector: Injector) { return injector.get(token); }; + factory.$inject = [NG2_INJECTOR]; + return factory; + } + /* @internal */ private compileNg2Components(compiler: Compiler, protoViewRefMap: ProtoViewRefMap): Promise { @@ -417,14 +531,18 @@ export class UpgradeAdapterRef { /* @internal */ private _readyFn: (upgradeAdapterRef?: UpgradeAdapterRef) => void = null; - public applicationRef: ApplicationRef = null; + public ng1RootScope: angular.IRootScopeService = null; public ng1Injector: angular.auto.IInjectorService = null; + public ng2ApplicationRef: ApplicationRef = null; + public ng2Injector: Injector = null; /* @internal */ private _bootstrapDone(applicationRef: ApplicationRef, ng1Injector: angular.auto.IInjectorService) { - this.applicationRef = applicationRef; + this.ng2ApplicationRef = applicationRef; + this.ng2Injector = applicationRef.injector; this.ng1Injector = ng1Injector; + this.ng1RootScope = ng1Injector.get(NG1_ROOT_SCOPE); this._readyFn && this._readyFn(this); } @@ -442,6 +560,6 @@ export class UpgradeAdapterRef { */ public dispose() { this.ng1Injector.get(NG1_ROOT_SCOPE).$destroy(); - this.applicationRef.dispose(); + this.ng2ApplicationRef.dispose(); } } diff --git a/modules/upgrade/test/integration_spec.ts b/modules/upgrade/test/integration_spec.ts index 51e08ddd4a..2fdfe813d6 100644 --- a/modules/upgrade/test/integration_spec.ts +++ b/modules/upgrade/test/integration_spec.ts @@ -11,7 +11,7 @@ import { xit, } from 'angular2/testing_internal'; -import {Component, Class, Inject, EventEmitter} from 'angular2/angular2'; +import {Component, Class, Inject, EventEmitter, ApplicationRef, provide} from 'angular2/angular2'; import {UpgradeAdapter} from 'upgrade/upgrade'; export function main() { @@ -468,6 +468,40 @@ export function main() { }); + describe('injection', () => { + function SomeToken() {} + + it('should export ng2 instance to ng1', inject([AsyncTestCompleter], (async) => { + var adapter = new UpgradeAdapter(); + var module = angular.module('myExample', []); + adapter.addProvider(provide(SomeToken, {useValue: 'correct_value'})); + module.factory('someToken', adapter.downgradeNg2Provider(SomeToken)); + adapter.bootstrap(html('
'), ['myExample']) + .ready((ref) => { + expect(ref.ng1Injector.get('someToken')).toBe('correct_value'); + ref.dispose(); + async.done(); + }); + })); + + it('should export ng1 instance to ng2', inject([AsyncTestCompleter], (async) => { + var adapter = new UpgradeAdapter(); + var module = angular.module('myExample', []); + module.value('testValue', 'secreteToken'); + adapter.upgradeNg1Provider('testValue'); + adapter.upgradeNg1Provider('testValue', {asToken: 'testToken'}); + adapter.upgradeNg1Provider('testValue', {asToken: String}); + adapter.bootstrap(html('
'), ['myExample']) + .ready((ref) => { + expect(ref.ng2Injector.get('testValue')).toBe('secreteToken'); + expect(ref.ng2Injector.get(String)).toBe('secreteToken'); + expect(ref.ng2Injector.get('testToken')).toBe('secreteToken'); + ref.dispose(); + async.done(); + }); + })); + }); + describe('examples', () => { it('should verify UpgradeAdapter example', inject([AsyncTestCompleter], (async) => { var adapter = new UpgradeAdapter();