From 059e8faae2ac27b1bea2f7a16f203e2415eb952f Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Mon, 12 Oct 2015 21:32:41 -0700 Subject: [PATCH] refactor(ngUpgrade): renames and docs BREAKING CHANGE: - Changes the terminology to Adapter and upgrade/downgrade - Removes the Module from the public API to prevent confusion --- modules/upgrade/src/angular.d.ts | 2 +- ...ng2_facade.ts => downgrade_ng2_adapter.ts} | 2 +- modules/upgrade/src/upgrade_adapter.ts | 253 ++++++++++++++++++ modules/upgrade/src/upgrade_module.ts | 196 -------------- .../{ng1_facade.ts => upgrade_ng1_adapter.ts} | 12 +- modules/upgrade/test/integration_spec.ts | 231 +++++++++------- modules/upgrade/upgrade.ts | 2 +- 7 files changed, 398 insertions(+), 300 deletions(-) rename modules/upgrade/src/{ng2_facade.ts => downgrade_ng2_adapter.ts} (99%) create mode 100644 modules/upgrade/src/upgrade_adapter.ts delete mode 100644 modules/upgrade/src/upgrade_module.ts rename modules/upgrade/src/{ng1_facade.ts => upgrade_ng1_adapter.ts} (92%) diff --git a/modules/upgrade/src/angular.d.ts b/modules/upgrade/src/angular.d.ts index e6013f0a49..c756f38a6a 100644 --- a/modules/upgrade/src/angular.d.ts +++ b/modules/upgrade/src/angular.d.ts @@ -52,7 +52,7 @@ declare namespace angular { assign(context: any, value: any): any; } function element(e: Element): IAugmentedJQuery; - function bootstrap(e: Element, modules: IModule[], config: IAngularBootstrapConfig); + function bootstrap(e: Element, modules: string[], config: IAngularBootstrapConfig); namespace auto { interface IInjectorService { diff --git a/modules/upgrade/src/ng2_facade.ts b/modules/upgrade/src/downgrade_ng2_adapter.ts similarity index 99% rename from modules/upgrade/src/ng2_facade.ts rename to modules/upgrade/src/downgrade_ng2_adapter.ts index 7af8a9514e..8d4612920a 100644 --- a/modules/upgrade/src/ng2_facade.ts +++ b/modules/upgrade/src/downgrade_ng2_adapter.ts @@ -18,7 +18,7 @@ const INITIAL_VALUE = { __UNINITIALIZED__: true }; -export class Ng2ComponentFacade { +export class DowngradeNg2ComponentAdapter { component: any = null; inputChangeCount: number = 0; inputChanges: {[key: string]: SimpleChange} = null; diff --git a/modules/upgrade/src/upgrade_adapter.ts b/modules/upgrade/src/upgrade_adapter.ts new file mode 100644 index 0000000000..cb1765829b --- /dev/null +++ b/modules/upgrade/src/upgrade_adapter.ts @@ -0,0 +1,253 @@ +/// + +import { + bind, + provide, + platform, + ApplicationRef, + AppViewManager, + Compiler, + Injector, + NgZone, + PlatformRef, + ProtoViewRef, + Type +} from 'angular2/angular2'; +import {applicationDomBindings} from 'angular2/src/core/application_common'; +import {applicationCommonBindings} from 'angular2/src/core/application_ref'; +import {compilerProviders} from 'angular2/src/core/compiler/compiler'; + +import {getComponentInfo, ComponentInfo} from './metadata'; +import {onError} from './util'; +import { + NG1_COMPILE, + NG1_INJECTOR, + NG1_PARSE, + NG1_ROOT_SCOPE, + NG1_REQUIRE_INJECTOR_REF, + NG1_SCOPE, + NG2_APP_VIEW_MANAGER, + NG2_COMPILER, + NG2_INJECTOR, + NG2_PROTO_VIEW_REF_MAP, + NG2_ZONE, + REQUIRE_INJECTOR +} from './constants'; +import {DowngradeNg2ComponentAdapter} from './downgrade_ng2_adapter'; +import {UpgradeNg1ComponentAdapterBuilder} from './upgrade_ng1_adapter'; + +var upgradeCount: number = 0; + +/** + * Use `UpgradeAdapter` to allow AngularJS v1 and Angular v2 to coexist in a single application. + * + * The `UpgradeAdapter` allows: + * 1. creation of Angular v2 component from AngularJS v1 component directive + * (See [UpgradeAdapter#upgradeNg1Component()]) + * 2. creation of AngularJS v1 directive from Angular v2 component. + * (See [UpgradeAdapter#downgradeNg2Component()]) + * 3. Bootstrapping of a hybrid Angular application which contains both of the frameworks + * coexisting in a single application. + * + * ## Mental Model + * + * When reasoning about how a hybrid application works it is useful to have a mental model which + * describes what is happening and explains what is happening at the lowest level. + * + * 1. There are two independent frameworks running in a single application, each framework treats + * the other as a black box. + * 2. Each DOM element on the page is owned exactly by one framework. Whichever framework + * instantiated the element is the owner. Each framework only updates/interacts with its own + * DOM elements and ignores others. + * 3. AngularJS v1 directives always execute inside AngularJS v1 framework codebase regardless of + * where they are instantiated. + * 4. Angular v2 components always execute inside Angular v2 framework codebase regardless of + * where they are instantiated. + * 5. An AngularJS v1 component can be upgraded to an Angular v2 component. This creates an + * Angular v2 directive, which bootstraps the AngularJS v1 component directive in that location. + * 6. An Angular v2 component can be downgraded to an AngularJS v1 component directive. This creates + * an AngularJS v1 directive, which bootstraps the Angular v2 component in that location. + * 7. Whenever an adapter component is instantiated the host element is owned by the the framework + * doing the instantiation. The other framework then instantiates and owns the view for that + * component. This implies that component bindings will always follow the semantics of the + * instantiation framework, but with Angular v2 syntax. + * 8. AngularJS v1 is always bootstrapped first and owns the bottom most view. + * + * ## Example + * + * ``` + * var adapter = new UpgradeAdapter(); + * var module = angular.module('myExample', []); + * + * module.directive('ng1', function() { + * return { + * scope: { title: '@' }, + * template: 'ng1[Hello {{title}}!]()' + * }; + * }); + * + * + * @Component({ + * selector: 'ng2', + * inputs: ['name'], + * template: 'ng2[transclude]()', + * directives: [adapter.upgradeNg1Component('ng1')] + * }) + * class Ng2 { + * } + * + * document.body = 'project'; + * + * adapter.bootstrap(document.body, ['myExample']).ready(function() { + * expect(document.body.textContent).toEqual( + * "ng2[ng1[Hello World!](transclude)](project)"); + * }); + * ``` + */ +export class UpgradeAdapter { + /* @internal */ + private idPrefix: string = `NG2_UPGRADE_${upgradeCount++}_`; + /* @internal */ + private upgradedComponents: Type[] = []; + /* @internal */ + private downgradedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder} = {}; + + downgradeNg2Component(type: Type): Function { + this.upgradedComponents.push(type); + var info: ComponentInfo = getComponentInfo(type); + return ng1ComponentDirective(info, `${this.idPrefix}${info.selector}_c`); + } + + upgradeNg1Component(name: string): Type { + if ((this.downgradedComponents).hasOwnProperty(name)) { + return this.downgradedComponents[name].type; + } else { + return (this.downgradedComponents[name] = new UpgradeNg1ComponentAdapterBuilder(name)).type; + } + } + + bootstrap(element: Element, modules?: any[], + config?: angular.IAngularBootstrapConfig): UpgradeRef { + var upgrade = new UpgradeRef(); + var ng1Injector: angular.auto.IInjectorService = null; + var platformRef: PlatformRef = platform(); + var applicationRef: ApplicationRef = platformRef.application([ + applicationCommonBindings(), + applicationDomBindings(), + compilerProviders(), + provide(NG1_INJECTOR, {useFactory: () => ng1Injector}), + provide(NG1_COMPILE, {useFactory: () => ng1Injector.get(NG1_COMPILE)}) + ]); + var injector: Injector = applicationRef.injector; + var ngZone: NgZone = injector.get(NgZone); + var compiler: Compiler = injector.get(Compiler); + var delayApplyExps: Function[] = []; + var original$applyFn: Function; + var rootScopePrototype: any; + var rootScope: angular.IRootScopeService; + var protoViewRefMap: ProtoViewRefMap = {}; + var ng1Module = angular.module(this.idPrefix, modules); + ng1Module.value(NG2_INJECTOR, injector) + .value(NG2_ZONE, ngZone) + .value(NG2_COMPILER, compiler) + .value(NG2_PROTO_VIEW_REF_MAP, protoViewRefMap) + .value(NG2_APP_VIEW_MANAGER, injector.get(AppViewManager)) + .config([ + '$provide', + (provide) => { + provide.decorator(NG1_ROOT_SCOPE, [ + '$delegate', + function(rootScopeDelegate: angular.IRootScopeService) { + rootScopePrototype = rootScopeDelegate.constructor.prototype; + if (rootScopePrototype.hasOwnProperty('$apply')) { + original$applyFn = rootScopePrototype.$apply; + rootScopePrototype.$apply = (exp) => delayApplyExps.push(exp); + } else { + throw new Error("Failed to find '$apply' on '$rootScope'!"); + } + return rootScope = rootScopeDelegate; + } + ]); + } + ]) + .run([ + '$injector', + '$rootScope', + (injector: angular.auto.IInjectorService, rootScope: angular.IRootScopeService) => { + ng1Injector = injector; + ngZone.overrideOnTurnDone(() => rootScope.$apply()); + UpgradeNg1ComponentAdapterBuilder.resolve(this.downgradedComponents, injector); + } + ]); + + angular.element(element).data(NG1_REQUIRE_INJECTOR_REF, injector); + ngZone.run(() => { angular.bootstrap(element, [this.idPrefix], config); }); + this.compileNg2Components(compiler, protoViewRefMap) + .then((protoViewRefMap: ProtoViewRefMap) => { + ngZone.run(() => { + rootScopePrototype.$apply = original$applyFn; // restore original $apply + while (delayApplyExps.length) { + rootScope.$apply(delayApplyExps.shift()); + } + upgrade.readyFn && upgrade.readyFn(); + }); + }); + return upgrade; + } + + /* @internal */ + private compileNg2Components(compiler: Compiler, + protoViewRefMap: ProtoViewRefMap): Promise { + var promises: Array> = []; + var types = this.upgradedComponents; + for (var i = 0; i < types.length; i++) { + promises.push(compiler.compileInHost(types[i])); + } + return Promise.all(promises).then((protoViews: Array) => { + var types = this.upgradedComponents; + for (var i = 0; i < protoViews.length; i++) { + protoViewRefMap[getComponentInfo(types[i]).selector] = protoViews[i]; + } + return protoViewRefMap; + }, onError); + } +} + +interface ProtoViewRefMap { + [selector: string]: ProtoViewRef +} + +function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function { + directiveFactory.$inject = [NG2_PROTO_VIEW_REF_MAP, NG2_APP_VIEW_MANAGER, NG1_PARSE]; + function directiveFactory(protoViewRefMap: ProtoViewRefMap, viewManager: AppViewManager, + parse: angular.IParseService): angular.IDirective { + var protoView: ProtoViewRef = protoViewRefMap[info.selector]; + if (!protoView) throw new Error('Expecting ProtoViewRef for: ' + info.selector); + var idCount = 0; + return { + restrict: 'E', + require: REQUIRE_INJECTOR, + link: { + post: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes, + parentInjector: any, transclude: angular.ITranscludeFunction): void => { + var domElement = element[0]; + var facade = new DowngradeNg2ComponentAdapter(idPrefix + (idCount++), info, element, + attrs, scope, parentInjector, + parse, viewManager, protoView); + facade.setupInputs(); + facade.bootstrapNg2(); + facade.projectContent(); + facade.setupOutputs(); + facade.registerCleanup(); + } + } + }; + } + return directiveFactory; +} + +export class UpgradeRef { + readyFn: Function; + + ready(fn: Function) { this.readyFn = fn; } +} diff --git a/modules/upgrade/src/upgrade_module.ts b/modules/upgrade/src/upgrade_module.ts deleted file mode 100644 index f51164367a..0000000000 --- a/modules/upgrade/src/upgrade_module.ts +++ /dev/null @@ -1,196 +0,0 @@ -/// - -import { - bind, - provide, - platform, - ApplicationRef, - AppViewManager, - Compiler, - Injector, - NgZone, - PlatformRef, - ProtoViewRef, - Type -} from 'angular2/angular2'; -import {applicationDomBindings} from 'angular2/src/core/application_common'; -import {applicationCommonBindings} from 'angular2/src/core/application_ref'; -import {compilerProviders} from 'angular2/src/core/compiler/compiler'; - -import {getComponentInfo, ComponentInfo} from './metadata'; -import {onError} from './util'; -import { - NG1_COMPILE, - NG1_INJECTOR, - NG1_PARSE, - NG1_ROOT_SCOPE, - NG1_REQUIRE_INJECTOR_REF, - NG1_SCOPE, - NG2_APP_VIEW_MANAGER, - NG2_COMPILER, - NG2_INJECTOR, - NG2_PROTO_VIEW_REF_MAP, - NG2_ZONE, - REQUIRE_INJECTOR -} from './constants'; -import {Ng2ComponentFacade} from './ng2_facade'; -import {ExportedNg1Component} from './ng1_facade'; - -var moduleCount: number = 0; - -export function createUpgradeModule(): UpgradeModule { - var prefix = `NG2_UPGRADE_m${moduleCount++}_`; - return new UpgradeModule(prefix, angular.module(prefix, [])); -} - -export class UpgradeModule { - importedNg2Components: Type[] = []; - exportedNg1Components: {[name: string]: ExportedNg1Component} = {} - - constructor(public idPrefix: string, public ng1Module: angular.IModule) {} - - importNg2Component(type: Type): UpgradeModule { - this.importedNg2Components.push(type); - var info: ComponentInfo = getComponentInfo(type); - var factory: Function = ng1ComponentDirective(info, `${this.idPrefix}${info.selector}_c`); - this.ng1Module.directive(info.selector, factory); - return this; - } - - exportAsNg2Component(name: string): Type { - if ((this.exportedNg1Components).hasOwnProperty(name)) { - return this.exportedNg1Components[name].type; - } else { - return (this.exportedNg1Components[name] = new ExportedNg1Component(name)).type; - } - } - - bootstrap(element: Element, modules?: any[], - config?: angular.IAngularBootstrapConfig): UpgradeRef { - var upgrade = new UpgradeRef(); - var ng1Injector: angular.auto.IInjectorService = null; - var bindings = [ - applicationCommonBindings(), - applicationDomBindings(), - compilerProviders(), - provide(NG1_INJECTOR, {useFactory: () => ng1Injector}), - provide(NG1_COMPILE, {useFactory: () => ng1Injector.get(NG1_COMPILE)}) - ]; - - var platformRef: PlatformRef = platform(); - var applicationRef: ApplicationRef = platformRef.application(bindings); - var injector: Injector = applicationRef.injector; - var ngZone: NgZone = injector.get(NgZone); - var compiler: Compiler = injector.get(Compiler); - var delayApplyExps: Function[] = []; - var original$applyFn: Function; - var rootScopePrototype: any; - var rootScope: angular.IRootScopeService; - var protoViewRefMap: ProtoViewRefMap = {}; - ngZone.run(() => { - this.ng1Module.value(NG2_INJECTOR, injector) - .value(NG2_ZONE, ngZone) - .value(NG2_COMPILER, compiler) - .value(NG2_PROTO_VIEW_REF_MAP, protoViewRefMap) - .value(NG2_APP_VIEW_MANAGER, injector.get(AppViewManager)) - .config([ - '$provide', - (provide) => { - provide.decorator(NG1_ROOT_SCOPE, [ - '$delegate', - function(rootScopeDelegate: angular.IRootScopeService) { - rootScopePrototype = rootScopeDelegate.constructor.prototype; - if (rootScopePrototype.hasOwnProperty('$apply')) { - original$applyFn = rootScopePrototype.$apply; - rootScopePrototype.$apply = (exp) => delayApplyExps.push(exp); - } else { - throw new Error("Failed to find '$apply' on '$rootScope'!"); - } - return rootScope = rootScopeDelegate; - } - ]); - } - ]) - .run([ - '$injector', - '$rootScope', - (injector: angular.auto.IInjectorService, rootScope: angular.IRootScopeService) => { - ng1Injector = injector; - ngZone.overrideOnTurnDone(() => rootScope.$apply()); - ExportedNg1Component.resolve(this.exportedNg1Components, injector); - } - ]); - - modules = modules ? [].concat(modules) : []; - modules.push(this.idPrefix); - angular.element(element).data(NG1_REQUIRE_INJECTOR_REF, injector); - angular.bootstrap(element, modules, config); - }); - this.compileNg2Components(compiler, protoViewRefMap) - .then((protoViewRefMap: ProtoViewRefMap) => { - ngZone.run(() => { - rootScopePrototype.$apply = original$applyFn; // restore original $apply - while (delayApplyExps.length) { - rootScope.$apply(delayApplyExps.shift()); - } - upgrade.readyFn && upgrade.readyFn(); - }); - }); - return upgrade; - } - - private compileNg2Components(compiler: Compiler, - protoViewRefMap: ProtoViewRefMap): Promise { - var promises: Array> = []; - var types = this.importedNg2Components; - for (var i = 0; i < types.length; i++) { - promises.push(compiler.compileInHost(types[i])); - } - return Promise.all(promises).then((protoViews: Array) => { - var types = this.importedNg2Components; - for (var i = 0; i < protoViews.length; i++) { - protoViewRefMap[getComponentInfo(types[i]).selector] = protoViews[i]; - } - return protoViewRefMap; - }, onError); - } -} - -interface ProtoViewRefMap { - [selector: string]: ProtoViewRef -} - -function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function { - directiveFactory.$inject = [NG2_PROTO_VIEW_REF_MAP, NG2_APP_VIEW_MANAGER, NG1_PARSE]; - function directiveFactory(protoViewRefMap: ProtoViewRefMap, viewManager: AppViewManager, - parse: angular.IParseService): angular.IDirective { - var protoView: ProtoViewRef = protoViewRefMap[info.selector]; - if (!protoView) throw new Error('Expecting ProtoViewRef for: ' + info.selector); - var idCount = 0; - return { - restrict: 'E', - require: REQUIRE_INJECTOR, - link: { - post: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes, - parentInjector: any, transclude: angular.ITranscludeFunction): void => { - var domElement = element[0]; - var facade = - new Ng2ComponentFacade(idPrefix + (idCount++), info, element, attrs, scope, - parentInjector, parse, viewManager, protoView); - facade.setupInputs(); - facade.bootstrapNg2(); - facade.projectContent(); - facade.setupOutputs(); - facade.registerCleanup(); - } - } - }; - } - return directiveFactory; -} - -export class UpgradeRef { - readyFn: Function; - - ready(fn: Function) { this.readyFn = fn; } -} diff --git a/modules/upgrade/src/ng1_facade.ts b/modules/upgrade/src/upgrade_ng1_adapter.ts similarity index 92% rename from modules/upgrade/src/ng1_facade.ts rename to modules/upgrade/src/upgrade_ng1_adapter.ts index e42619e40b..8c5aba32a1 100644 --- a/modules/upgrade/src/ng1_facade.ts +++ b/modules/upgrade/src/upgrade_ng1_adapter.ts @@ -16,7 +16,7 @@ const INITIAL_VALUE = { }; -export class ExportedNg1Component { +export class UpgradeNg1ComponentAdapterBuilder { type: Type; inputs: string[] = []; inputsRename: string[] = []; @@ -38,9 +38,9 @@ export class ExportedNg1Component { ElementRef, function(compile: angular.ICompileService, scope: angular.IScope, elementRef: ElementRef) { - return new Ng1ComponentFacade(compile, scope, elementRef, self.inputs, - self.outputs, self.propertyOutputs, - self.checkProperties, self.propertyMap); + return new UpgradeNg1ComponentAdapter(compile, scope, elementRef, self.inputs, + self.outputs, self.propertyOutputs, + self.checkProperties, self.propertyMap); } ], onChanges: function() { /* needs to be here for ng2 to properly detect it */ }, @@ -94,7 +94,7 @@ export class ExportedNg1Component { } } - static resolve(exportedComponents: {[name: string]: ExportedNg1Component}, + static resolve(exportedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder}, injector: angular.auto.IInjectorService) { for (var name in exportedComponents) { if ((exportedComponents).hasOwnProperty(name)) { @@ -105,7 +105,7 @@ export class ExportedNg1Component { } } -class Ng1ComponentFacade implements OnChanges, DoCheck { +class UpgradeNg1ComponentAdapter implements OnChanges, DoCheck { componentScope: angular.IScope = null; checkLastValues: any[] = []; diff --git a/modules/upgrade/test/integration_spec.ts b/modules/upgrade/test/integration_spec.ts index 19fdada5f3..d8a3146c96 100644 --- a/modules/upgrade/test/integration_spec.ts +++ b/modules/upgrade/test/integration_spec.ts @@ -11,95 +11,100 @@ import { xit, } from 'angular2/testing_internal'; -import {Component, View, Inject, EventEmitter} from 'angular2/angular2'; -import {createUpgradeModule, UpgradeModule} from 'upgrade/upgrade'; +import {Component, Inject, EventEmitter} from 'angular2/angular2'; +import {UpgradeAdapter} from 'upgrade/upgrade'; export function main() { - describe('upgrade: ng1 to ng2', () => { + describe('adapter: ng1 to ng2', () => { it('should have angular 1 loaded', () => expect(angular.version.major).toBe(1)); it('should instantiate ng2 in ng1 template and project content', inject([AsyncTestCompleter], (async) => { - var Ng2 = Component({selector: 'ng2'}) - .View({template: `{{ 'NG2' }}()`}) + var ng1Module = angular.module('ng1', []); + var Ng2 = Component({selector: 'ng2', template: `{{ 'NG2' }}()`}) .Class({constructor: function() {}}); var element = html("
{{ 'ng1[' }}~{{ 'ng-content' }}~{{ ']' }}
"); - var upgradeModule: UpgradeModule = createUpgradeModule(); - upgradeModule.importNg2Component(Ng2); - upgradeModule.bootstrap(element).ready(() => { - expect(document.body.textContent).toEqual("ng1[NG2(~ng-content~)]"); - async.done(); - }); + var adapter: UpgradeAdapter = new UpgradeAdapter(); + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); + adapter.bootstrap(element, ['ng1']) + .ready(() => { + expect(document.body.textContent).toEqual("ng1[NG2(~ng-content~)]"); + async.done(); + }); })); it('should instantiate ng1 in ng2 template and project content', inject([AsyncTestCompleter], (async) => { - var upgrMod: UpgradeModule = createUpgradeModule(); + var adapter: UpgradeAdapter = new UpgradeAdapter(); + var ng1Module = angular.module('ng1', []); - var Ng2 = Component({selector: 'ng2-1'}) - .View({ - template: `{{ 'ng2(' }}{{'transclude'}}{{ ')' }}`, - directives: [upgrMod.exportAsNg2Component('ng1')] - }) - .Class({constructor: function() {}}); + var Ng2 = Component({ + selector: 'ng2', + template: `{{ 'ng2(' }}{{'transclude'}}{{ ')' }}`, + directives: [adapter.upgradeNg1Component('ng1')] + }).Class({constructor: function() {}}); - upgrMod.ng1Module.directive('ng1', () => { + ng1Module.directive('ng1', () => { return {transclude: true, template: '{{ "ng1" }}()'}; }); - upgrMod.importNg2Component(Ng2); + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); - var element = html("
{{'ng1('}}{{')'}}
"); + var element = html("
{{'ng1('}}{{')'}}
"); - upgrMod.bootstrap(element).ready(() => { - expect(document.body.textContent).toEqual("ng1(ng2(ng1(transclude)))"); - async.done(); - }); + adapter.bootstrap(element, ['ng1']) + .ready(() => { + expect(document.body.textContent).toEqual("ng1(ng2(ng1(transclude)))"); + async.done(); + }); })); describe('scope/component change-detection', () => { it('should interleave scope and component expressions', inject([AsyncTestCompleter], (async) => { + var ng1Module = angular.module('ng1', []); var log = []; var l = function(value) { log.push(value); return value + ';'; }; - var upgrMod: UpgradeModule = createUpgradeModule(); + var adapter: UpgradeAdapter = new UpgradeAdapter(); - upgrMod.ng1Module.directive('ng1a', () => { return {template: "{{ l('ng1a') }}"}; }); - upgrMod.ng1Module.directive('ng1b', () => { return {template: "{{ l('ng1b') }}"}; }); - upgrMod.ng1Module.run(($rootScope) => { + ng1Module.directive('ng1a', () => { return {template: "{{ l('ng1a') }}"}; }); + ng1Module.directive('ng1b', () => { return {template: "{{ l('ng1b') }}"}; }); + ng1Module.run(($rootScope) => { $rootScope.l = l; $rootScope.reset = () => log.length = 0; }); - upgrMod.importNg2Component( - Component({selector: 'ng2'}) - .View({ - template: `{{l('2A')}}{{l('2B')}}{{l('2C')}}`, - directives: [ - upgrMod.exportAsNg2Component('ng1a'), - upgrMod.exportAsNg2Component('ng1b') - ] - }) - .Class({constructor: function() { this.l = l; }})); + var Ng2 = + Component({ + selector: 'ng2', + template: `{{l('2A')}}{{l('2B')}}{{l('2C')}}`, + directives: + [adapter.upgradeNg1Component('ng1a'), adapter.upgradeNg1Component('ng1b')] + }).Class({constructor: function() { this.l = l; }}); + + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); var element = html("
{{reset(); l('1A');}}{{l('1B')}}{{l('1C')}}
"); - upgrMod.bootstrap(element).ready(() => { - expect(document.body.textContent).toEqual("1A;2A;ng1a;2B;ng1b;2C;1C;"); - // https://github.com/angular/angular.js/issues/12983 - expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); - async.done(); - }); + adapter.bootstrap(element, ['ng1']) + .ready(() => { + expect(document.body.textContent).toEqual("1A;2A;ng1a;2B;ng1b;2C;1C;"); + // https://github.com/angular/angular.js/issues/12983 + expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); + async.done(); + }); })); }); describe('binding from ng1 to ng2', () => { it('should bind properties, events', inject([AsyncTestCompleter], (async) => { - var upgrMod: UpgradeModule = createUpgradeModule(); - upgrMod.ng1Module.run(($rootScope) => { + var adapter: UpgradeAdapter = new UpgradeAdapter(); + var ng1Module = angular.module('ng1', []); + + ng1Module.run(($rootScope) => { $rootScope.dataA = 'A'; $rootScope.dataB = 'B'; $rootScope.modelA = 'initModelA'; @@ -107,7 +112,7 @@ export function main() { $rootScope.eventA = '?'; $rootScope.eventB = '?'; }); - upgrMod.importNg2Component( + var Ng2 = Component({ selector: 'ng2', inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], @@ -116,15 +121,12 @@ export function main() { 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' - ] + ], + template: "ignore: {{ignore}}; " + + "literal: {{literal}}; interpolate: {{interpolate}}; " + + "oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; " + + "twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{onChangesCount}})" }) - .View({ - template: - "ignore: {{ignore}}; " + - "literal: {{literal}}; interpolate: {{interpolate}}; " + - "oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; " + - "twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{onChangesCount}})" - }) .Class({ constructor: function() { this.onChangesCount = 0; @@ -185,7 +187,8 @@ export function main() { throw new Error('Called too many times! ' + JSON.stringify(changes)); } } - })); + }); + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); var element = html(`
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
`); - upgrMod.bootstrap(element).ready(() => { - expect(multiTrim(document.body.textContent)) - .toEqual( - "ignore: -; " + "literal: Text; interpolate: Hello world; " + - "oneWayA: A; oneWayB: B; twoWayA: initModelA; twoWayB: initModelB; (1) | " + - "modelA: initModelA; modelB: initModelB; eventA: ?; eventB: ?;"); - setTimeout(() => { - // we need to do setTimeout, because the EventEmitter uses setTimeout to schedule - // events, and so without this we would not see the events processed. - expect(multiTrim(document.body.textContent)) - .toEqual("ignore: -; " + "literal: Text; interpolate: Hello world; " + - "oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | " + - "modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;"); - async.done(); - }); - }); + adapter.bootstrap(element, ['ng1']) + .ready(() => { + expect(multiTrim(document.body.textContent)) + .toEqual( + "ignore: -; " + "literal: Text; interpolate: Hello world; " + + "oneWayA: A; oneWayB: B; twoWayA: initModelA; twoWayB: initModelB; (1) | " + + "modelA: initModelA; modelB: initModelB; eventA: ?; eventB: ?;"); + setTimeout(() => { + // we need to do setTimeout, because the EventEmitter uses setTimeout to schedule + // events, and so without this we would not see the events processed. + expect(multiTrim(document.body.textContent)) + .toEqual("ignore: -; " + "literal: Text; interpolate: Hello world; " + + "oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | " + + "modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;"); + async.done(); + }); + }); })); }); describe('binding from ng2 to ng1', () => { it('should bind properties, events', inject([AsyncTestCompleter], (async) => { - var upgrMod = createUpgradeModule(); + var adapter = new UpgradeAdapter(); + var ng1Module = angular.module('ng1', []); + var ng1 = function() { return { template: 'Hello {{fullName}}; A: {{dataA}}; B: {{dataB}}; | ', @@ -233,17 +239,17 @@ export function main() { } } }; - upgrMod.ng1Module.directive('ng1', ng1); - var ng2 = - Component({selector: 'ng2'}) - .View({ - template: - '' + - '' + - '{{event}}-{{last}}, {{first}}', - directives: [upgrMod.exportAsNg2Component('ng1')] - }) + ng1Module.directive('ng1', ng1); + var Ng2 = + Component({ + selector: 'ng2', + template: + '' + + '' + + '{{event}}-{{last}}, {{first}}', + directives: [adapter.upgradeNg1Component('ng1')] + }) .Class({ constructor: function() { this.first = 'Victor'; @@ -251,18 +257,53 @@ export function main() { this.event = '?'; } }); - upgrMod.importNg2Component(ng2); + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); var element = html(`
`); - upgrMod.bootstrap(element).ready(() => { - // we need to do setTimeout, because the EventEmitter uses setTimeout to schedule - // events, and so without this we would not see the events processed. - setTimeout(() => { - expect(multiTrim(document.body.textContent)) - .toEqual( - "Hello SAVKIN, Victor; A: VICTOR; B: SAVKIN; | Hello TEST; A: First; B: Last; | WORKS-SAVKIN, Victor"); - async.done(); - }, 0); + adapter.bootstrap(element, ['ng1']) + .ready(() => { + // we need to do setTimeout, because the EventEmitter uses setTimeout to schedule + // events, and so without this we would not see the events processed. + setTimeout(() => { + expect(multiTrim(document.body.textContent)) + .toEqual( + "Hello SAVKIN, Victor; A: VICTOR; B: SAVKIN; | Hello TEST; A: First; B: Last; | WORKS-SAVKIN, Victor"); + async.done(); + }, 0); + }); + })); + }); + + describe('examples', () => { + it('should verify UpgradeAdapter example', inject([AsyncTestCompleter], (async) => { + var adapter = new UpgradeAdapter(); + var module = angular.module('myExample', []); + + module.directive('ng1', function() { + return { + scope: {title: '@'}, + transclude: true, template: 'ng1[Hello {{title}}!]()' + }; }); + + + var Ng2 = + Component({ + selector: 'ng2', + inputs: ['name'], + template: 'ng2[transclude]()', + directives: [adapter.upgradeNg1Component('ng1')] + }).Class({constructor: function() {}}); + + module.directive('ng2', adapter.downgradeNg2Component(Ng2)); + + document.body.innerHTML = 'project'; + + adapter.bootstrap(document.body, ['myExample']) + .ready(function() { + expect(multiTrim(document.body.textContent)) + .toEqual("ng2[ng1[Hello World!](transclude)](project)"); + async.done(); + }); })); }); diff --git a/modules/upgrade/upgrade.ts b/modules/upgrade/upgrade.ts index 60af6cdc3f..9ae1c1f71c 100644 --- a/modules/upgrade/upgrade.ts +++ b/modules/upgrade/upgrade.ts @@ -1 +1 @@ -export {createUpgradeModule, UpgradeModule} from './src/upgrade_module'; +export {UpgradeAdapter} from './src/upgrade_adapter';