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
This commit is contained in:
Misko Hevery 2015-10-12 21:32:41 -07:00 committed by Miško Hevery
parent d7ab5d44a5
commit 059e8faae2
7 changed files with 398 additions and 300 deletions

View File

@ -52,7 +52,7 @@ declare namespace angular {
assign(context: any, value: any): any; assign(context: any, value: any): any;
} }
function element(e: Element): IAugmentedJQuery; function element(e: Element): IAugmentedJQuery;
function bootstrap(e: Element, modules: IModule[], config: IAngularBootstrapConfig); function bootstrap(e: Element, modules: string[], config: IAngularBootstrapConfig);
namespace auto { namespace auto {
interface IInjectorService { interface IInjectorService {

View File

@ -18,7 +18,7 @@ const INITIAL_VALUE = {
__UNINITIALIZED__: true __UNINITIALIZED__: true
}; };
export class Ng2ComponentFacade { export class DowngradeNg2ComponentAdapter {
component: any = null; component: any = null;
inputChangeCount: number = 0; inputChangeCount: number = 0;
inputChanges: {[key: string]: SimpleChange} = null; inputChanges: {[key: string]: SimpleChange} = null;

View File

@ -0,0 +1,253 @@
///<reference path="./angular.d.ts"/>
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}}!](<span ng-transclude></span>)'
* };
* });
*
*
* @Component({
* selector: 'ng2',
* inputs: ['name'],
* template: 'ng2[<ng1 [title]="name">transclude</ng1>](<ng-content></ng-content>)',
* directives: [adapter.upgradeNg1Component('ng1')]
* })
* class Ng2 {
* }
*
* document.body = '<ng2 name="World">project</ng2>';
*
* 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 ((<any>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<ProtoViewRefMap> {
var promises: Array<Promise<ProtoViewRef>> = [];
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<ProtoViewRef>) => {
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 = <any>element[0];
var facade = new DowngradeNg2ComponentAdapter(idPrefix + (idCount++), info, element,
attrs, scope, <Injector>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; }
}

View File

@ -1,196 +0,0 @@
///<reference path="./angular.d.ts"/>
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, <any>factory);
return this;
}
exportAsNg2Component(name: string): Type {
if ((<any>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<ProtoViewRefMap> {
var promises: Array<Promise<ProtoViewRef>> = [];
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<ProtoViewRef>) => {
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 = <any>element[0];
var facade =
new Ng2ComponentFacade(idPrefix + (idCount++), info, element, attrs, scope,
<Injector>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; }
}

View File

@ -16,7 +16,7 @@ const INITIAL_VALUE = {
}; };
export class ExportedNg1Component { export class UpgradeNg1ComponentAdapterBuilder {
type: Type; type: Type;
inputs: string[] = []; inputs: string[] = [];
inputsRename: string[] = []; inputsRename: string[] = [];
@ -38,9 +38,9 @@ export class ExportedNg1Component {
ElementRef, ElementRef,
function(compile: angular.ICompileService, scope: angular.IScope, function(compile: angular.ICompileService, scope: angular.IScope,
elementRef: ElementRef) { elementRef: ElementRef) {
return new Ng1ComponentFacade(compile, scope, elementRef, self.inputs, return new UpgradeNg1ComponentAdapter(compile, scope, elementRef, self.inputs,
self.outputs, self.propertyOutputs, self.outputs, self.propertyOutputs,
self.checkProperties, self.propertyMap); self.checkProperties, self.propertyMap);
} }
], ],
onChanges: function() { /* needs to be here for ng2 to properly detect it */ }, 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) { injector: angular.auto.IInjectorService) {
for (var name in exportedComponents) { for (var name in exportedComponents) {
if ((<any>exportedComponents).hasOwnProperty(name)) { if ((<any>exportedComponents).hasOwnProperty(name)) {
@ -105,7 +105,7 @@ export class ExportedNg1Component {
} }
} }
class Ng1ComponentFacade implements OnChanges, DoCheck { class UpgradeNg1ComponentAdapter implements OnChanges, DoCheck {
componentScope: angular.IScope = null; componentScope: angular.IScope = null;
checkLastValues: any[] = []; checkLastValues: any[] = [];

View File

@ -11,95 +11,100 @@ import {
xit, xit,
} from 'angular2/testing_internal'; } from 'angular2/testing_internal';
import {Component, View, Inject, EventEmitter} from 'angular2/angular2'; import {Component, Inject, EventEmitter} from 'angular2/angular2';
import {createUpgradeModule, UpgradeModule} from 'upgrade/upgrade'; import {UpgradeAdapter} from 'upgrade/upgrade';
export function main() { 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 have angular 1 loaded', () => expect(angular.version.major).toBe(1));
it('should instantiate ng2 in ng1 template and project content', it('should instantiate ng2 in ng1 template and project content',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
var Ng2 = Component({selector: 'ng2'}) var ng1Module = angular.module('ng1', []);
.View({template: `{{ 'NG2' }}(<ng-content></ng-content>)`}) var Ng2 = Component({selector: 'ng2', template: `{{ 'NG2' }}(<ng-content></ng-content>)`})
.Class({constructor: function() {}}); .Class({constructor: function() {}});
var element = html("<div>{{ 'ng1[' }}<ng2>~{{ 'ng-content' }}~</ng2>{{ ']' }}</div>"); var element = html("<div>{{ 'ng1[' }}<ng2>~{{ 'ng-content' }}~</ng2>{{ ']' }}</div>");
var upgradeModule: UpgradeModule = createUpgradeModule(); var adapter: UpgradeAdapter = new UpgradeAdapter();
upgradeModule.importNg2Component(Ng2); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
upgradeModule.bootstrap(element).ready(() => { adapter.bootstrap(element, ['ng1'])
expect(document.body.textContent).toEqual("ng1[NG2(~ng-content~)]"); .ready(() => {
async.done(); expect(document.body.textContent).toEqual("ng1[NG2(~ng-content~)]");
}); async.done();
});
})); }));
it('should instantiate ng1 in ng2 template and project content', it('should instantiate ng1 in ng2 template and project content',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
var upgrMod: UpgradeModule = createUpgradeModule(); var adapter: UpgradeAdapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
var Ng2 = Component({selector: 'ng2-1'}) var Ng2 = Component({
.View({ selector: 'ng2',
template: `{{ 'ng2(' }}<ng1>{{'transclude'}}</ng1>{{ ')' }}`, template: `{{ 'ng2(' }}<ng1>{{'transclude'}}</ng1>{{ ')' }}`,
directives: [upgrMod.exportAsNg2Component('ng1')] directives: [adapter.upgradeNg1Component('ng1')]
}) }).Class({constructor: function() {}});
.Class({constructor: function() {}});
upgrMod.ng1Module.directive('ng1', () => { ng1Module.directive('ng1', () => {
return {transclude: true, template: '{{ "ng1" }}(<ng-transclude></ng-transclude>)'}; return {transclude: true, template: '{{ "ng1" }}(<ng-transclude></ng-transclude>)'};
}); });
upgrMod.importNg2Component(Ng2); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html("<div>{{'ng1('}}<ng2-1></ng2-1>{{')'}}</div>"); var element = html("<div>{{'ng1('}}<ng2></ng2>{{')'}}</div>");
upgrMod.bootstrap(element).ready(() => { adapter.bootstrap(element, ['ng1'])
expect(document.body.textContent).toEqual("ng1(ng2(ng1(transclude)))"); .ready(() => {
async.done(); expect(document.body.textContent).toEqual("ng1(ng2(ng1(transclude)))");
}); async.done();
});
})); }));
describe('scope/component change-detection', () => { describe('scope/component change-detection', () => {
it('should interleave scope and component expressions', it('should interleave scope and component expressions',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
var ng1Module = angular.module('ng1', []);
var log = []; var log = [];
var l = function(value) { var l = function(value) {
log.push(value); log.push(value);
return value + ';'; return value + ';';
}; };
var upgrMod: UpgradeModule = createUpgradeModule(); var adapter: UpgradeAdapter = new UpgradeAdapter();
upgrMod.ng1Module.directive('ng1a', () => { return {template: "{{ l('ng1a') }}"}; }); ng1Module.directive('ng1a', () => { return {template: "{{ l('ng1a') }}"}; });
upgrMod.ng1Module.directive('ng1b', () => { return {template: "{{ l('ng1b') }}"}; }); ng1Module.directive('ng1b', () => { return {template: "{{ l('ng1b') }}"}; });
upgrMod.ng1Module.run(($rootScope) => { ng1Module.run(($rootScope) => {
$rootScope.l = l; $rootScope.l = l;
$rootScope.reset = () => log.length = 0; $rootScope.reset = () => log.length = 0;
}); });
upgrMod.importNg2Component( var Ng2 =
Component({selector: 'ng2'}) Component({
.View({ selector: 'ng2',
template: `{{l('2A')}}<ng1a></ng1a>{{l('2B')}}<ng1b></ng1b>{{l('2C')}}`, template: `{{l('2A')}}<ng1a></ng1a>{{l('2B')}}<ng1b></ng1b>{{l('2C')}}`,
directives: [ directives:
upgrMod.exportAsNg2Component('ng1a'), [adapter.upgradeNg1Component('ng1a'), adapter.upgradeNg1Component('ng1b')]
upgrMod.exportAsNg2Component('ng1b') }).Class({constructor: function() { this.l = l; }});
]
}) ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
.Class({constructor: function() { this.l = l; }}));
var element = html("<div>{{reset(); l('1A');}}<ng2>{{l('1B')}}</ng2>{{l('1C')}}</div>"); var element = html("<div>{{reset(); l('1A');}}<ng2>{{l('1B')}}</ng2>{{l('1C')}}</div>");
upgrMod.bootstrap(element).ready(() => { adapter.bootstrap(element, ['ng1'])
expect(document.body.textContent).toEqual("1A;2A;ng1a;2B;ng1b;2C;1C;"); .ready(() => {
// https://github.com/angular/angular.js/issues/12983 expect(document.body.textContent).toEqual("1A;2A;ng1a;2B;ng1b;2C;1C;");
expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); // https://github.com/angular/angular.js/issues/12983
async.done(); expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']);
}); async.done();
});
})); }));
}); });
describe('binding from ng1 to ng2', () => { describe('binding from ng1 to ng2', () => {
it('should bind properties, events', inject([AsyncTestCompleter], (async) => { it('should bind properties, events', inject([AsyncTestCompleter], (async) => {
var upgrMod: UpgradeModule = createUpgradeModule(); var adapter: UpgradeAdapter = new UpgradeAdapter();
upgrMod.ng1Module.run(($rootScope) => { var ng1Module = angular.module('ng1', []);
ng1Module.run(($rootScope) => {
$rootScope.dataA = 'A'; $rootScope.dataA = 'A';
$rootScope.dataB = 'B'; $rootScope.dataB = 'B';
$rootScope.modelA = 'initModelA'; $rootScope.modelA = 'initModelA';
@ -107,7 +112,7 @@ export function main() {
$rootScope.eventA = '?'; $rootScope.eventA = '?';
$rootScope.eventB = '?'; $rootScope.eventB = '?';
}); });
upgrMod.importNg2Component( var Ng2 =
Component({ Component({
selector: 'ng2', selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
@ -116,15 +121,12 @@ export function main() {
'eventB', 'eventB',
'twoWayAEmitter: twoWayAChange', 'twoWayAEmitter: twoWayAChange',
'twoWayBEmitter: twoWayBChange' '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({ .Class({
constructor: function() { constructor: function() {
this.onChangesCount = 0; this.onChangesCount = 0;
@ -185,7 +187,8 @@ export function main() {
throw new Error('Called too many times! ' + JSON.stringify(changes)); throw new Error('Called too many times! ' + JSON.stringify(changes));
} }
} }
})); });
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div> var element = html(`<div>
<ng2 literal="Text" interpolate="Hello {{'world'}}" <ng2 literal="Text" interpolate="Hello {{'world'}}"
bind-one-way-a="dataA" [one-way-b]="dataB" bind-one-way-a="dataA" [one-way-b]="dataB"
@ -193,29 +196,32 @@ export function main() {
on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2> on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2>
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}}; | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`); </div>`);
upgrMod.bootstrap(element).ready(() => { adapter.bootstrap(element, ['ng1'])
expect(multiTrim(document.body.textContent)) .ready(() => {
.toEqual( expect(multiTrim(document.body.textContent))
"ignore: -; " + "literal: Text; interpolate: Hello world; " + .toEqual(
"oneWayA: A; oneWayB: B; twoWayA: initModelA; twoWayB: initModelB; (1) | " + "ignore: -; " + "literal: Text; interpolate: Hello world; " +
"modelA: initModelA; modelB: initModelB; eventA: ?; eventB: ?;"); "oneWayA: A; oneWayB: B; twoWayA: initModelA; twoWayB: initModelB; (1) | " +
setTimeout(() => { "modelA: initModelA; modelB: initModelB; eventA: ?; eventB: ?;");
// we need to do setTimeout, because the EventEmitter uses setTimeout to schedule setTimeout(() => {
// events, and so without this we would not see the events processed. // we need to do setTimeout, because the EventEmitter uses setTimeout to schedule
expect(multiTrim(document.body.textContent)) // events, and so without this we would not see the events processed.
.toEqual("ignore: -; " + "literal: Text; interpolate: Hello world; " + expect(multiTrim(document.body.textContent))
"oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | " + .toEqual("ignore: -; " + "literal: Text; interpolate: Hello world; " +
"modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;"); "oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | " +
async.done(); "modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;");
}); async.done();
}); });
});
})); }));
}); });
describe('binding from ng2 to ng1', () => { describe('binding from ng2 to ng1', () => {
it('should bind properties, events', inject([AsyncTestCompleter], (async) => { it('should bind properties, events', inject([AsyncTestCompleter], (async) => {
var upgrMod = createUpgradeModule(); var adapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
var ng1 = function() { var ng1 = function() {
return { return {
template: 'Hello {{fullName}}; A: {{dataA}}; B: {{dataB}}; | ', template: 'Hello {{fullName}}; A: {{dataA}}; B: {{dataB}}; | ',
@ -233,17 +239,17 @@ export function main() {
} }
} }
}; };
upgrMod.ng1Module.directive('ng1', ng1); ng1Module.directive('ng1', ng1);
var ng2 = var Ng2 =
Component({selector: 'ng2'}) Component({
.View({ selector: 'ng2',
template: template:
'<ng1 full-name="{{last}}, {{first}}" [model-a]="first" [(model-b)]="last" ' + '<ng1 full-name="{{last}}, {{first}}" [model-a]="first" [(model-b)]="last" ' +
'(event)="event=$event"></ng1>' + '(event)="event=$event"></ng1>' +
'<ng1 full-name="{{\'TEST\'}}" model-a="First" model-b="Last"></ng1>' + '<ng1 full-name="{{\'TEST\'}}" model-a="First" model-b="Last"></ng1>' +
'{{event}}-{{last}}, {{first}}', '{{event}}-{{last}}, {{first}}',
directives: [upgrMod.exportAsNg2Component('ng1')] directives: [adapter.upgradeNg1Component('ng1')]
}) })
.Class({ .Class({
constructor: function() { constructor: function() {
this.first = 'Victor'; this.first = 'Victor';
@ -251,18 +257,53 @@ export function main() {
this.event = '?'; this.event = '?';
} }
}); });
upgrMod.importNg2Component(ng2); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div><ng2></ng2></div>`); var element = html(`<div><ng2></ng2></div>`);
upgrMod.bootstrap(element).ready(() => { adapter.bootstrap(element, ['ng1'])
// we need to do setTimeout, because the EventEmitter uses setTimeout to schedule .ready(() => {
// events, and so without this we would not see the events processed. // we need to do setTimeout, because the EventEmitter uses setTimeout to schedule
setTimeout(() => { // events, and so without this we would not see the events processed.
expect(multiTrim(document.body.textContent)) setTimeout(() => {
.toEqual( expect(multiTrim(document.body.textContent))
"Hello SAVKIN, Victor; A: VICTOR; B: SAVKIN; | Hello TEST; A: First; B: Last; | WORKS-SAVKIN, Victor"); .toEqual(
async.done(); "Hello SAVKIN, Victor; A: VICTOR; B: SAVKIN; | Hello TEST; A: First; B: Last; | WORKS-SAVKIN, Victor");
}, 0); 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}}!](<span ng-transclude></span>)'
};
}); });
var Ng2 =
Component({
selector: 'ng2',
inputs: ['name'],
template: 'ng2[<ng1 [title]="name">transclude</ng1>](<ng-content></ng-content>)',
directives: [adapter.upgradeNg1Component('ng1')]
}).Class({constructor: function() {}});
module.directive('ng2', adapter.downgradeNg2Component(Ng2));
document.body.innerHTML = '<ng2 name="World">project</ng2>';
adapter.bootstrap(document.body, ['myExample'])
.ready(function() {
expect(multiTrim(document.body.textContent))
.toEqual("ng2[ng1[Hello World!](transclude)](project)");
async.done();
});
})); }));
}); });

View File

@ -1 +1 @@
export {createUpgradeModule, UpgradeModule} from './src/upgrade_module'; export {UpgradeAdapter} from './src/upgrade_adapter';