chore(ngUpgrade): Move into Angular2

This is moving ngUpgrade into the main repository per #4838.
The ngUpgrade is published from the main import consistent with
https://docs.google.com/document/d/1rbVTKTYLz6p2smQNYI8h4-QN-m2PS6F3iQIDmSzn0Ww/edit#heading=h.6cxvr9awtf5r
Closes #4931
This commit is contained in:
Misko Hevery
2015-10-26 20:17:46 -07:00
parent 54f7e62c43
commit 1b78342e23
19 changed files with 183 additions and 175 deletions

View File

@ -2,3 +2,4 @@ export * from './core';
export * from './profile';
export * from './lifecycle_hooks';
export * from './bootstrap';
export * from './upgrade';

View File

@ -0,0 +1,123 @@
export interface IModule {
config(fn: any): IModule;
directive(selector: string, factory: any): IModule;
controller(name: string, type: any): IModule;
factory(key: string, factoryFn: any): IModule;
value(key: string, value: any): IModule;
run(a: any): void;
}
export interface ICompileService {
(element: Element | NodeList | string, transclude?: Function): ILinkFn;
}
export interface ILinkFn {
(scope: IScope, cloneAttachFn?: Function, options?: ILinkFnOptions): void;
}
export interface ILinkFnOptions {
parentBoundTranscludeFn?: Function;
transcludeControllers?: {[key: string]: any};
futureParentElement?: Node;
}
export interface IRootScopeService {
$new(isolate?: boolean): IScope;
$id: string;
$watch(expr: any, fn?: (a1?: any, a2?: any) => void): Function;
$apply(): any;
$apply(exp: string): any;
$apply(exp: Function): any;
$$childTail: IScope;
$$childHead: IScope;
$$nextSibling: IScope;
}
export interface IScope extends IRootScopeService {}
export interface IAngularBootstrapConfig {}
export interface IDirective {
compile?: IDirectiveCompileFn;
controller?: any;
controllerAs?: string;
bindToController?: boolean | Object;
link?: IDirectiveLinkFn | IDirectivePrePost;
name?: string;
priority?: number;
replace?: boolean;
require?: any;
restrict?: string;
scope?: any;
template?: any;
templateUrl?: any;
terminal?: boolean;
transclude?: any;
}
export interface IDirectiveCompileFn {
(templateElement: IAugmentedJQuery, templateAttributes: IAttributes,
transclude: ITranscludeFunction): IDirectivePrePost;
}
export interface IDirectivePrePost {
pre?: IDirectiveLinkFn;
post?: IDirectiveLinkFn;
}
export interface IDirectiveLinkFn {
(scope: IScope, instanceElement: IAugmentedJQuery, instanceAttributes: IAttributes,
controller: any, transclude: ITranscludeFunction): void;
}
export interface IAttributes { $observe(attr: string, fn: (v: string) => void): void; }
export interface ITranscludeFunction {
// If the scope is provided, then the cloneAttachFn must be as well.
(scope: IScope, cloneAttachFn: ICloneAttachFunction): IAugmentedJQuery;
// If one argument is provided, then it's assumed to be the cloneAttachFn.
(cloneAttachFn?: ICloneAttachFunction): IAugmentedJQuery;
}
export interface ICloneAttachFunction {
// Let's hint but not force cloneAttachFn's signature
(clonedElement?: IAugmentedJQuery, scope?: IScope): any;
}
export interface IAugmentedJQuery {
bind(name: string, fn: () => void): void;
data(name: string, value?: any): any;
inheritedData(name: string, value?: any): any;
contents(): IAugmentedJQuery;
parent(): IAugmentedJQuery;
length: number;
[index: number]: Node;
}
export interface IParseService { (expression: string): ICompiledExpression; }
export interface ICompiledExpression { assign(context: any, value: any): any; }
export interface IHttpBackendService {
(method: string, url: string, post?: any, callback?: Function, headers?: any, timeout?: number,
withCredentials?: boolean): void;
}
export interface ICacheObject {
put<T>(key: string, value?: T): T;
get(key: string): any;
}
export interface ITemplateCacheService extends ICacheObject {}
export interface IControllerService {
(controllerConstructor: Function, locals?: any, later?: any, ident?: any): any;
(controllerName: string, locals?: any): any;
}
export interface IInjectorService { get(key: string): any; }
function noNg() {
throw new Error('AngularJS v1.x is not loaded!');
}
var angular: {
bootstrap: (e: Element, modules: string[], config: IAngularBootstrapConfig) => void,
module: (prefix: string, dependencies?: string[]) => IModule,
element: (e: Element) => IAugmentedJQuery,
version: {major: number}
} = <any>{bootstrap: noNg, module: noNg, element: noNg, version: noNg};
try {
if (window.hasOwnProperty('angular')) {
angular = (<any>window).angular;
}
} catch (e) {
// ignore in CJS mode.
}
export var bootstrap = angular.bootstrap;
export var module = angular.module;
export var element = angular.element;
export var version = angular.version;

View File

@ -0,0 +1,15 @@
export const NG2_APP_VIEW_MANAGER = 'ng2.AppViewManager';
export const NG2_COMPILER = 'ng2.Compiler';
export const NG2_INJECTOR = 'ng2.Injector';
export const NG2_PROTO_VIEW_REF_MAP = 'ng2.ProtoViewRefMap';
export const NG2_ZONE = 'ng2.NgZone';
export const NG1_CONTROLLER = '$controller';
export const NG1_SCOPE = '$scope';
export const NG1_ROOT_SCOPE = '$rootScope';
export const NG1_COMPILE = '$compile';
export const NG1_HTTP_BACKEND = '$httpBackend';
export const NG1_INJECTOR = '$injector';
export const NG1_PARSE = '$parse';
export const NG1_TEMPLATE_CACHE = '$templateCache';
export const REQUIRE_INJECTOR = '^' + NG2_INJECTOR;

View File

@ -0,0 +1,171 @@
import {
bind,
provide,
AppViewManager,
ChangeDetectorRef,
HostViewRef,
Injector,
ProtoViewRef,
SimpleChange
} from 'angular2/angular2';
import {NG1_SCOPE} from './constants';
import {ComponentInfo} from './metadata';
import Element = protractor.Element;
import * as angular from './angular_js';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
};
export class DowngradeNg2ComponentAdapter {
component: any = null;
inputChangeCount: number = 0;
inputChanges: {[key: string]: SimpleChange} = null;
hostViewRef: HostViewRef = null;
changeDetector: ChangeDetectorRef = null;
componentScope: angular.IScope;
childNodes: Node[];
contentInserctionPoint: Node = null;
constructor(private id: string, private info: ComponentInfo,
private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes,
private scope: angular.IScope, private parentInjector: Injector,
private parse: angular.IParseService, private viewManager: AppViewManager,
private protoView: ProtoViewRef) {
(<any>this.element[0]).id = id;
this.componentScope = scope.$new();
this.childNodes = <Node[]><any>element.contents();
}
bootstrapNg2() {
var childInjector = this.parentInjector.resolveAndCreateChild(
[provide(NG1_SCOPE, {useValue: this.componentScope})]);
this.hostViewRef =
this.viewManager.createRootHostView(this.protoView, '#' + this.id, childInjector);
var renderer: any = (<any>this.hostViewRef).render;
var hostElement = this.viewManager.getHostElement(this.hostViewRef);
this.changeDetector = this.hostViewRef.changeDetectorRef;
this.component = this.viewManager.getComponent(hostElement);
this.contentInserctionPoint = renderer.rootContentInsertionPoints[0];
}
setupInputs(): void {
var attrs = this.attrs;
var inputs = this.info.inputs;
for (var i = 0; i < inputs.length; i++) {
var input = inputs[i];
var expr = null;
if (attrs.hasOwnProperty(input.attr)) {
var observeFn = ((prop) => {
var prevValue = INITIAL_VALUE;
return (value) => {
if (this.inputChanges !== null) {
this.inputChangeCount++;
this.inputChanges[prop] =
new Ng1Change(value, prevValue === INITIAL_VALUE ? value : prevValue);
prevValue = value;
}
this.component[prop] = value;
};
})(input.prop);
attrs.$observe(input.attr, observeFn);
} else if (attrs.hasOwnProperty(input.bindAttr)) {
expr = attrs[input.bindAttr];
} else if (attrs.hasOwnProperty(input.bracketAttr)) {
expr = attrs[input.bracketAttr];
} else if (attrs.hasOwnProperty(input.bindonAttr)) {
expr = attrs[input.bindonAttr];
} else if (attrs.hasOwnProperty(input.bracketParenAttr)) {
expr = attrs[input.bracketParenAttr];
}
if (expr != null) {
var watchFn = ((prop) => (value, prevValue) => {
if (this.inputChanges != null) {
this.inputChangeCount++;
this.inputChanges[prop] = new Ng1Change(prevValue, value);
}
this.component[prop] = value;
})(input.prop);
this.componentScope.$watch(expr, watchFn);
}
}
var prototype = this.info.type.prototype;
if (prototype && prototype.onChanges) {
// Detect: OnChanges interface
this.inputChanges = {};
this.componentScope.$watch(() => this.inputChangeCount, () => {
var inputChanges = this.inputChanges;
this.inputChanges = {};
this.component.onChanges(inputChanges);
});
}
this.componentScope.$watch(() => this.changeDetector && this.changeDetector.detectChanges());
}
projectContent() {
var childNodes = this.childNodes;
if (this.contentInserctionPoint) {
var parent = this.contentInserctionPoint.parentNode;
for (var i = 0, ii = childNodes.length; i < ii; i++) {
parent.insertBefore(childNodes[i], this.contentInserctionPoint);
}
}
}
setupOutputs() {
var attrs = this.attrs;
var outputs = this.info.outputs;
for (var j = 0; j < outputs.length; j++) {
var output = outputs[j];
var expr = null;
var assignExpr = false;
var bindonAttr =
output.bindonAttr ? output.bindonAttr.substring(0, output.bindonAttr.length - 6) : null;
var bracketParenAttr =
output.bracketParenAttr ?
`[(${output.bracketParenAttr.substring(2, output.bracketParenAttr.length - 8)})]` :
null;
if (attrs.hasOwnProperty(output.onAttr)) {
expr = attrs[output.onAttr];
} else if (attrs.hasOwnProperty(output.parenAttr)) {
expr = attrs[output.parenAttr];
} else if (attrs.hasOwnProperty(bindonAttr)) {
expr = attrs[bindonAttr];
assignExpr = true;
} else if (attrs.hasOwnProperty(bracketParenAttr)) {
expr = attrs[bracketParenAttr];
assignExpr = true;
}
if (expr != null && assignExpr != null) {
var getter = this.parse(expr);
var setter = getter.assign;
if (assignExpr && !setter) {
throw new Error(`Expression '${expr}' is not assignable!`);
}
var emitter = this.component[output.prop];
if (emitter) {
emitter.subscribe({
next: assignExpr ? ((setter) => (value) => setter(this.scope, value))(setter) :
((getter) => (value) => getter(this.scope, {$event: value}))(getter)
});
} else {
throw new Error(`Missing emitter '${output.prop}' on component '${this.info.selector}'!`);
}
}
}
}
registerCleanup() {
this.element.bind('$remove', () => this.viewManager.destroyRootHostView(this.hostViewRef));
}
}
class Ng1Change implements SimpleChange {
constructor(public previousValue: any, public currentValue: any) {}
isFirstChange(): boolean { return this.previousValue === this.currentValue; }
}

View File

@ -0,0 +1,62 @@
import {Type, ComponentMetadata, DirectiveResolver, DirectiveMetadata} from 'angular2/angular2';
import {stringify} from './util';
var COMPONENT_SELECTOR = /^[\w|-]*$/;
var SKEWER_CASE = /-(\w)/g;
var directiveResolver = new DirectiveResolver();
export interface AttrProp {
prop: string;
attr: string;
bracketAttr: string;
bracketParenAttr: string;
parenAttr: string;
onAttr: string;
bindAttr: string;
bindonAttr: string;
}
export interface ComponentInfo {
type: Type;
selector: string;
inputs: AttrProp[];
outputs: AttrProp[];
}
export function getComponentInfo(type: Type): ComponentInfo {
var resolvedMetadata: DirectiveMetadata = directiveResolver.resolve(type);
var selector = resolvedMetadata.selector;
if (!selector.match(COMPONENT_SELECTOR)) {
throw new Error('Only selectors matching element names are supported, got: ' + selector);
}
var selector = selector.replace(SKEWER_CASE, (all, letter: string) => letter.toUpperCase());
return {
type: type,
selector: selector,
inputs: parseFields(resolvedMetadata.inputs),
outputs: parseFields(resolvedMetadata.outputs)
};
}
export function parseFields(names: string[]): AttrProp[] {
var attrProps: AttrProp[] = [];
if (names) {
for (var i = 0; i < names.length; i++) {
var parts = names[i].split(':');
var prop = parts[0].trim();
var attr = (parts[1] || parts[0]).trim();
var capitalAttr = attr.charAt(0).toUpperCase() + attr.substr(1);
attrProps.push(<AttrProp>{
prop: prop,
attr: attr,
bracketAttr: `[${attr}]`,
parenAttr: `(${attr})`,
bracketParenAttr: `[(${attr})]`,
onAttr: `on${capitalAttr}`,
bindAttr: `bind${capitalAttr}`,
bindonAttr: `bindon${capitalAttr}`
});
}
}
return attrProps;
}

View File

@ -0,0 +1,563 @@
import {
bind,
provide,
platform,
ApplicationRef,
AppViewManager,
Compiler,
Inject,
Injector,
NgZone,
PlatformRef,
ProtoViewRef,
Provider,
Type
} from 'angular2/angular2';
import {applicationDomProviders} from 'angular2/src/core/application_common';
import {applicationCommonProviders} from 'angular2/src/core/application_ref';
import {compilerProviders} from 'angular2/src/core/compiler/compiler';
import {getComponentInfo, ComponentInfo} from './metadata';
import {onError, controllerKey} from './util';
import {
NG1_COMPILE,
NG1_INJECTOR,
NG1_PARSE,
NG1_ROOT_SCOPE,
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';
import * as angular from './angular_js';
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. The syntax is always that of Angular v2 syntax.
* 8. AngularJS v1 is always bootstrapped first and owns the bottom most view.
* 9. The new application is running in Angular v2 zone, and therefore it no longer needs calls to
* `$apply()`.
*
* ### Example
*
* ```
* var adapter = new UpgradeAdapter();
* var module = angular.module('myExample', []);
* module.directive('ng2', adapter.downgradeNg2Component(Ng2));
*
* 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.innerHTML = '<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} = {};
/* @internal */
private providers: Array<Type | Provider | any[]> = [];
/**
* Allows Angular v2 Component to be used from AngularJS v1.
*
* Use `downgradeNg2Component` to create an AngularJS v1 Directive Definition Factory from
* Angular v2 Component. The adapter will bootstrap Angular v2 component from within the
* AngularJS v1 template.
*
* ## Mental Model
*
* 1. The component is instantiated by being listed in AngularJS v1 template. This means that the
* host element is controlled by AngularJS v1, but the component's view will be controlled by
* Angular v2.
* 2. Even thought the component is instantiated in AngularJS v1, it will be using Angular v2
* syntax. This has to be done, this way because we must follow Angular v2 components do not
* declare how the attributes should be interpreted.
*
* ## Supported Features
*
* - Bindings:
* - Attribute: `<comp name="World">`
* - Interpolation: `<comp greeting="Hello {{name}}!">`
* - Expression: `<comp [name]="username">`
* - Event: `<comp (close)="doSomething()">`
* - Content projection: yes
*
* ### Example
*
* ```
* var adapter = new UpgradeAdapter();
* var module = angular.module('myExample', []);
* module.directive('greet', adapter.downgradeNg2Component(Greeter));
*
* @Component({
* selector: 'greet',
* template: '{{salutation}} {{name}}! - <ng-content></ng-content>'
* })
* class Greeter {
* @Input() salutation: string;
* @Input() name: string;
* }
*
* document.body.innerHTML =
* 'ng1 template: <greet salutation="Hello" [name]="world">text</greet>';
*
* adapter.bootstrap(document.body, ['myExample']).ready(function() {
* expect(document.body.textContent).toEqual("ng1 template: Hello world! - text");
* });
* ```
*/
downgradeNg2Component(type: Type): Function {
this.upgradedComponents.push(type);
var info: ComponentInfo = getComponentInfo(type);
return ng1ComponentDirective(info, `${this.idPrefix}${info.selector}_c`);
}
/**
* Allows AngularJS v1 Component to be used from Angular v2.
*
* Use `upgradeNg1Component` to create an Angular v2 component from AngularJS v1 Component
* directive. The adapter will bootstrap AngularJS v1 component from within the Angular v2
* template.
*
* ## Mental Model
*
* 1. The component is instantiated by being listed in Angular v2 template. This means that the
* host element is controlled by Angular v2, but the component's view will be controlled by
* AngularJS v1.
*
* ## Supported Features
*
* - Bindings:
* - Attribute: `<comp name="World">`
* - Interpolation: `<comp greeting="Hello {{name}}!">`
* - Expression: `<comp [name]="username">`
* - Event: `<comp (close)="doSomething()">`
* - Transclusion: yes
* - Only some of the features of
* [Directive Definition Object](https://docs.angularjs.org/api/ng/service/$compile) are
* supported:
* - `compile`: not supported because the host element is owned by Angular v2, which does
* not allow modifying DOM structure during compilation.
* - `controller`: supported. (NOTE: injection of `$attrs` and `$transclude` is not supported.)
* - `controllerAs': supported.
* - `bindToController': supported.
* - `link': supported. (NOTE: only pre-link function is supported.)
* - `name': supported.
* - `priority': ignored.
* - `replace': not supported.
* - `require`: supported.
* - `restrict`: must be set to 'E'.
* - `scope`: supported.
* - `template`: supported.
* - `templateUrl`: supported.
* - `terminal`: ignored.
* - `transclude`: supported.
*
*
* ### Example
*
* ```
* var adapter = new UpgradeAdapter();
* var module = angular.module('myExample', []);
*
* module.directive('greet', function() {
* return {
* scope: {salutation: '=', name: '=' },
* template: '{{salutation}} {{name}}! - <span ng-transclude></span>'
* };
* });
*
* module.directive('ng2', adapter.downgradeNg2Component(Ng2));
*
* @Component({
* selector: 'ng2',
* template: 'ng2 template: <greet salutation="Hello" [name]="world">text</greet>'
* directives: [adapter.upgradeNg1Component('greet')]
* })
* class Ng2 {
* }
*
* document.body.innerHTML = '<ng2></ng2>';
*
* adapter.bootstrap(document.body, ['myExample']).ready(function() {
* expect(document.body.textContent).toEqual("ng2 template: Hello world! - text");
* });
* ```
*/
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 a hybrid AngularJS v1 / Angular v2 application.
*
* This `bootstrap` method is a direct replacement (takes same arguments) for AngularJS v1
* [`bootstrap`](https://docs.angularjs.org/api/ng/function/angular.bootstrap) method. Unlike
* AngularJS v1, this bootstrap is asynchronous.
*
* ### Example
*
* ```
* var adapter = new UpgradeAdapter();
* var module = angular.module('myExample', []);
* module.directive('ng2', adapter.downgradeNg2Component(Ng2));
*
* 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.innerHTML = '<ng2 name="World">project</ng2>';
*
* adapter.bootstrap(document.body, ['myExample']).ready(function() {
* expect(document.body.textContent).toEqual(
* "ng2[ng1[Hello World!](transclude)](project)");
* });
* ```
*/
bootstrap(element: Element, modules?: any[],
config?: angular.IAngularBootstrapConfig): UpgradeAdapterRef {
var upgrade = new UpgradeAdapterRef();
var ng1Injector: angular.IInjectorService = null;
var platformRef: PlatformRef = platform();
var applicationRef: ApplicationRef = platformRef.application([
applicationCommonProviders(),
applicationDomProviders(),
compilerProviders(),
provide(NG1_INJECTOR, {useFactory: () => ng1Injector}),
provide(NG1_COMPILE, {useFactory: () => ng1Injector.get(NG1_COMPILE)}),
this.providers
]);
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);
var ng1compilePromise: Promise<any> = null;
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.IInjectorService, rootScope: angular.IRootScopeService) => {
ng1Injector = injector;
ngZone.overrideOnTurnDone(() => rootScope.$apply());
ng1compilePromise =
UpgradeNg1ComponentAdapterBuilder.resolve(this.downgradedComponents, injector);
}
]);
angular.element(element).data(controllerKey(NG2_INJECTOR), injector);
ngZone.run(() => { angular.bootstrap(element, [this.idPrefix], config); });
Promise.all([this.compileNg2Components(compiler, protoViewRefMap), ng1compilePromise])
.then(() => {
ngZone.run(() => {
if (rootScopePrototype) {
rootScopePrototype.$apply = original$applyFn; // restore original $apply
while (delayApplyExps.length) {
rootScope.$apply(delayApplyExps.shift());
}
(<any>upgrade)._bootstrapDone(applicationRef, ng1Injector);
rootScopePrototype = null;
}
});
}, 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 = '<app></app>'
* 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.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); };
(<any>factory).$inject = [NG2_INJECTOR];
return factory;
}
/* @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 {
(<any>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;
}
/**
* Use `UgradeAdapterRef` to control a hybrid AngularJS v1 / Angular v2 application.
*/
export class UpgradeAdapterRef {
/* @internal */
private _readyFn: (upgradeAdapterRef?: UpgradeAdapterRef) => void = null;
public ng1RootScope: angular.IRootScopeService = null;
public ng1Injector: angular.IInjectorService = null;
public ng2ApplicationRef: ApplicationRef = null;
public ng2Injector: Injector = null;
/* @internal */
private _bootstrapDone(applicationRef: ApplicationRef, ng1Injector: angular.IInjectorService) {
this.ng2ApplicationRef = applicationRef;
this.ng2Injector = applicationRef.injector;
this.ng1Injector = ng1Injector;
this.ng1RootScope = ng1Injector.get(NG1_ROOT_SCOPE);
this._readyFn && this._readyFn(this);
}
/**
* Register a callback function which is notified upon successful hybrid AngularJS v1 / Angular v2
* application has been bootstrapped.
*
* The `ready` callback function is invoked inside the Angular v2 zone, therefore it does not
* require a call to `$apply()`.
*/
public ready(fn: (upgradeAdapterRef?: UpgradeAdapterRef) => void) { this._readyFn = fn; }
/**
* Dispose of running hybrid AngularJS v1 / Angular v2 application.
*/
public dispose() {
this.ng1Injector.get(NG1_ROOT_SCOPE).$destroy();
this.ng2ApplicationRef.dispose();
}
}

View File

@ -0,0 +1,299 @@
import {
Directive,
DoCheck,
ElementRef,
EventEmitter,
Inject,
OnChanges,
SimpleChange,
Type
} from 'angular2/angular2';
import {
NG1_COMPILE,
NG1_SCOPE,
NG1_HTTP_BACKEND,
NG1_TEMPLATE_CACHE,
NG1_CONTROLLER
} from './constants';
import {controllerKey} from './util';
import * as angular from './angular_js';
const CAMEL_CASE = /([A-Z])/g;
const INITIAL_VALUE = {
__UNINITIALIZED__: true
};
const NOT_SUPPORTED: any = 'NOT_SUPPORTED';
export class UpgradeNg1ComponentAdapterBuilder {
type: Type;
inputs: string[] = [];
inputsRename: string[] = [];
outputs: string[] = [];
outputsRename: string[] = [];
propertyOutputs: string[] = [];
checkProperties: string[] = [];
propertyMap: {[name: string]: string} = {};
linkFn: angular.ILinkFn = null;
directive: angular.IDirective = null;
$controller: angular.IControllerService = null;
constructor(public name: string) {
var selector = name.replace(CAMEL_CASE, (all, next: string) => '-' + next.toLowerCase());
var self = this;
this.type =
Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename})
.Class({
constructor: [
new Inject(NG1_SCOPE),
ElementRef,
function(scope: angular.IScope, elementRef: ElementRef) {
return new UpgradeNg1ComponentAdapter(
self.linkFn, scope, self.directive, elementRef, self.$controller, self.inputs,
self.outputs, self.propertyOutputs, self.checkProperties, self.propertyMap);
}
],
onChanges: function() { /* needs to be here for ng2 to properly detect it */ },
doCheck: function() { /* needs to be here for ng2 to properly detect it */ }
});
}
extractDirective(injector: angular.IInjectorService): angular.IDirective {
var directives: angular.IDirective[] = injector.get(this.name + 'Directive');
if (directives.length > 1) {
throw new Error('Only support single directive definition for: ' + this.name);
}
var directive = directives[0];
if (directive.replace) this.notSupported('replace');
if (directive.terminal) this.notSupported('terminal');
var link = directive.link;
if (typeof link == 'object') {
if ((<angular.IDirectivePrePost>link).post) this.notSupported('link.post');
}
return directive;
}
private notSupported(feature: string) {
throw new Error(`Upgraded directive '${this.name}' does not support '${feature}'.`);
}
extractBindings() {
var scope = this.directive.scope;
if (typeof scope == 'object') {
for (var name in scope) {
if ((<any>scope).hasOwnProperty(name)) {
var localName = scope[name];
var type = localName.charAt(0);
localName = localName.substr(1) || name;
var outputName = 'output_' + name;
var outputNameRename = outputName + ': ' + name;
var outputNameRenameChange = outputName + ': ' + name + 'Change';
var inputName = 'input_' + name;
var inputNameRename = inputName + ': ' + name;
switch (type) {
case '=':
this.propertyOutputs.push(outputName);
this.checkProperties.push(localName);
this.outputs.push(outputName);
this.outputsRename.push(outputNameRenameChange);
this.propertyMap[outputName] = localName;
// don't break; let it fall through to '@'
case '@':
this.inputs.push(inputName);
this.inputsRename.push(inputNameRename);
this.propertyMap[inputName] = localName;
break;
case '&':
this.outputs.push(outputName);
this.outputsRename.push(outputNameRename);
this.propertyMap[outputName] = localName;
break;
default:
var json = JSON.stringify(scope);
throw new Error(
`Unexpected mapping '${type}' in '${json}' in '${this.name}' directive.`);
}
}
}
}
}
compileTemplate(compile: angular.ICompileService, templateCache: angular.ITemplateCacheService,
httpBackend: angular.IHttpBackendService): Promise<any> {
if (this.directive.template) {
this.linkFn = compileHtml(this.directive.template);
} else if (this.directive.templateUrl) {
var url = this.directive.templateUrl;
var html = templateCache.get(url);
if (html !== undefined) {
this.linkFn = compileHtml(html);
} else {
return new Promise((resolve, err) => {
httpBackend('GET', url, null, (status, response) => {
if (status == 200) {
resolve(this.linkFn = compileHtml(templateCache.put(url, response)));
} else {
err(`GET ${url} returned ${status}: ${response}`);
}
});
});
}
} else {
throw new Error(`Directive '${this.name}' is not a component, it is missing template.`);
}
return null;
function compileHtml(html): angular.ILinkFn {
var div = document.createElement('div');
div.innerHTML = html;
return compile(div.childNodes);
}
}
static resolve(exportedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder},
injector: angular.IInjectorService): Promise<any> {
var promises = [];
var compile: angular.ICompileService = injector.get(NG1_COMPILE);
var templateCache: angular.ITemplateCacheService = injector.get(NG1_TEMPLATE_CACHE);
var httpBackend: angular.IHttpBackendService = injector.get(NG1_HTTP_BACKEND);
var $controller: angular.IControllerService = injector.get(NG1_CONTROLLER);
for (var name in exportedComponents) {
if ((<any>exportedComponents).hasOwnProperty(name)) {
var exportedComponent = exportedComponents[name];
exportedComponent.directive = exportedComponent.extractDirective(injector);
exportedComponent.$controller = $controller;
exportedComponent.extractBindings();
var promise = exportedComponent.compileTemplate(compile, templateCache, httpBackend);
if (promise) promises.push(promise);
}
}
return Promise.all(promises);
}
}
class UpgradeNg1ComponentAdapter implements OnChanges, DoCheck {
destinationObj: any = null;
checkLastValues: any[] = [];
constructor(linkFn: angular.ILinkFn, scope: angular.IScope, private directive: angular.IDirective,
elementRef: ElementRef, $controller: angular.IControllerService,
private inputs: string[], private outputs: string[], private propOuts: string[],
private checkProperties: string[], private propertyMap: {[key: string]: string}) {
var element: Element = elementRef.nativeElement;
var childNodes: Node[] = [];
var childNode;
while (childNode = element.firstChild) {
element.removeChild(childNode);
childNodes.push(childNode);
}
var componentScope = scope.$new(!!directive.scope);
var $element = angular.element(element);
var controllerType = directive.controller;
var controller: any = null;
if (controllerType) {
var locals = {$scope: componentScope, $element: $element};
controller = $controller(controllerType, locals, null, directive.controllerAs);
$element.data(controllerKey(directive.name), controller);
}
var link = directive.link;
if (typeof link == 'object') link = (<angular.IDirectivePrePost>link).pre;
if (link) {
var attrs: angular.IAttributes = NOT_SUPPORTED;
var transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED;
var linkController = this.resolveRequired($element, directive.require);
(<angular.IDirectiveLinkFn>directive.link)(componentScope, $element, attrs, linkController,
transcludeFn);
}
this.destinationObj = directive.bindToController && controller ? controller : componentScope;
linkFn(componentScope, (clonedElement: Node[], scope: angular.IScope) => {
for (var i = 0, ii = clonedElement.length; i < ii; i++) {
element.appendChild(clonedElement[i]);
}
}, {parentBoundTranscludeFn: (scope, cloneAttach) => { cloneAttach(childNodes); }});
for (var i = 0; i < inputs.length; i++) {
this[inputs[i]] = null;
}
for (var j = 0; j < outputs.length; j++) {
var emitter = this[outputs[j]] = new EventEmitter();
this.setComponentProperty(outputs[j], ((emitter) => (value) => emitter.next(value))(emitter));
}
for (var k = 0; k < propOuts.length; k++) {
this[propOuts[k]] = new EventEmitter();
this.checkLastValues.push(INITIAL_VALUE);
}
}
onChanges(changes: {[name: string]: SimpleChange}) {
for (var name in changes) {
if ((<Object>changes).hasOwnProperty(name)) {
var change: SimpleChange = changes[name];
this.setComponentProperty(name, change.currentValue);
}
}
}
doCheck(): number {
var count = 0;
var destinationObj = this.destinationObj;
var lastValues = this.checkLastValues;
var checkProperties = this.checkProperties;
for (var i = 0; i < checkProperties.length; i++) {
var value = destinationObj[checkProperties[i]];
var last = lastValues[i];
if (value !== last) {
if (typeof value == 'number' && isNaN(value) && typeof last == 'number' && isNaN(last)) {
// ignore because NaN != NaN
} else {
var eventEmitter: EventEmitter<any> = this[this.propOuts[i]];
eventEmitter.next(lastValues[i] = value);
}
}
}
return count;
}
setComponentProperty(name: string, value: any) {
this.destinationObj[this.propertyMap[name]] = value;
}
private resolveRequired($element: angular.IAugmentedJQuery, require: string | string[]): any {
if (!require) {
return undefined;
} else if (typeof require == 'string') {
var name: string = <string>require;
var isOptional = false;
var startParent = false;
var searchParents = false;
var ch: string;
if (name.charAt(0) == '?') {
isOptional = true;
name = name.substr(1);
}
if (name.charAt(0) == '^') {
searchParents = true;
name = name.substr(1);
}
if (name.charAt(0) == '^') {
startParent = true;
name = name.substr(1);
}
var key = controllerKey(name);
if (startParent) $element = $element.parent();
var dep = searchParents ? $element.inheritedData(key) : $element.data(key);
if (!dep && !isOptional) {
throw new Error(`Can not locate '${require}' in '${this.directive.name}'.`);
}
return dep;
} else if (require instanceof Array) {
var deps = [];
for (var i = 0; i < require.length; i++) {
deps.push(this.resolveRequired($element, require[i]));
}
return deps;
}
throw new Error(
`Directive '${this.directive.name}' require syntax unrecognized: ${this.directive.require}`);
}
}

View File

@ -0,0 +1,16 @@
export function stringify(obj: any): string {
if (typeof obj == 'function') return obj.name || obj.toString();
return '' + obj;
}
export function onError(e: any) {
// TODO: (misko): We seem to not have a stack trace here!
console.log(e, e.stack);
throw e;
}
export function controllerKey(name: string): string {
return '$' + name + 'Controller';
}

View File

@ -25,7 +25,7 @@ import {SymbolsDiff} from './symbol_inspector/symbol_differ';
// =================================================================================================
// =================================================================================================
var NG_API = [
var NG_ALL = [
'APP_COMPONENT',
'APP_ID',
'AbstractProviderError',
@ -1450,6 +1450,22 @@ var NG_API = [
'Stream.where():dart',
];
var NG_UPGRADE = [
'UpgradeAdapter:js',
'UpgradeAdapter.addProvider():js',
'UpgradeAdapter.bootstrap():js',
'UpgradeAdapter.compileNg2Components():js',
'UpgradeAdapter.downgradeNg2Component():js',
'UpgradeAdapter.downgradeNg2Provider():js',
'UpgradeAdapter.upgradeNg1Component():js',
'UpgradeAdapter.upgradeNg1Provider():js',
'UpgradeAdapterRef:js',
'UpgradeAdapterRef.dispose():js',
'UpgradeAdapterRef.ready():js'
];
var NG_API = [].concat(NG_ALL).concat(NG_UPGRADE);
export function main() {
/**
var x = getSymbolsFromLibrary('ng');

View File

@ -0,0 +1,81 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xdescribe,
xit,
} from 'angular2/testing_internal';
import {Component, View} from 'angular2/angular2';
import {getComponentInfo, parseFields} from 'angular2/src/upgrade/metadata';
import {DOM} from 'angular2/src/core/dom/dom_adapter';
export function main() {
if (!DOM.supportsDOMEvents()) return;
describe('upgrade metadata', () => {
it('should extract component selector', () => {
expect(getComponentInfo(ElementNameComponent).selector).toEqual('elementNameDashed');
});
describe('errors', () => {
it('should throw on missing selector', () => {
expect(() => getComponentInfo(AttributeNameComponent))
.toThrowErrorWith(
"Only selectors matching element names are supported, got: [attr-name]");
});
it('should throw on non element names', () => {
expect(() => getComponentInfo(NoAnnotationComponent))
.toThrowErrorWith("No Directive annotation found on NoAnnotationComponent");
});
});
describe('parseFields', () => {
it('should process nulls', () => { expect(parseFields(null)).toEqual([]); });
it('should process values', () => {
expect(parseFields([' name ', ' prop : attr ']))
.toEqual([
{
prop: 'name',
attr: 'name',
bracketAttr: '[name]',
parenAttr: '(name)',
bracketParenAttr: '[(name)]',
onAttr: 'onName',
bindAttr: 'bindName',
bindonAttr: 'bindonName'
},
{
prop: 'prop',
attr: 'attr',
bracketAttr: '[attr]',
parenAttr: '(attr)',
bracketParenAttr: '[(attr)]',
onAttr: 'onAttr',
bindAttr: 'bindAttr',
bindonAttr: 'bindonAttr'
}
]);
});
})
});
}
@Component({selector: 'element-name-dashed'})
@View({template: ``})
class ElementNameComponent {
}
@Component({selector: '[attr-name]'})
@View({template: ``})
class AttributeNameComponent {
}
class NoAnnotationComponent {}

View File

@ -0,0 +1,562 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xdescribe,
xit,
} from 'angular2/testing_internal';
import {DOM} from 'angular2/src/core/dom/dom_adapter';
import {Component, Class, Inject, EventEmitter, ApplicationRef, provide} from 'angular2/angular2';
import {UpgradeAdapter} from 'angular2/upgrade';
import * as angular from 'angular2/src/upgrade/angular_js';
export function main() {
if (!DOM.supportsDOMEvents()) return;
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 ng1Module = angular.module('ng1', []);
var Ng2 = Component({selector: 'ng2', template: `{{ 'NG2' }}(<ng-content></ng-content>)`})
.Class({constructor: function() {}});
var element = html("<div>{{ 'ng1[' }}<ng2>~{{ 'ng-content' }}~</ng2>{{ ']' }}</div>");
var adapter: UpgradeAdapter = new UpgradeAdapter();
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
expect(document.body.textContent).toEqual("ng1[NG2(~ng-content~)]");
ref.dispose();
async.done();
});
}));
it('should instantiate ng1 in ng2 template and project content',
inject([AsyncTestCompleter], (async) => {
var adapter: UpgradeAdapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
var Ng2 = Component({
selector: 'ng2',
template: `{{ 'ng2(' }}<ng1>{{'transclude'}}</ng1>{{ ')' }}`,
directives: [adapter.upgradeNg1Component('ng1')]
}).Class({constructor: function() {}});
ng1Module.directive('ng1', () => {
return {transclude: true, template: '{{ "ng1" }}(<ng-transclude></ng-transclude>)'};
});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html("<div>{{'ng1('}}<ng2></ng2>{{')'}}</div>");
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
expect(document.body.textContent).toEqual("ng1(ng2(ng1(transclude)))");
ref.dispose();
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 adapter: UpgradeAdapter = new UpgradeAdapter();
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;
});
var Ng2 =
Component({
selector: 'ng2',
template: `{{l('2A')}}<ng1a></ng1a>{{l('2B')}}<ng1b></ng1b>{{l('2C')}}`,
directives:
[adapter.upgradeNg1Component('ng1a'), adapter.upgradeNg1Component('ng1b')]
}).Class({constructor: function() { this.l = l; }});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html("<div>{{reset(); l('1A');}}<ng2>{{l('1B')}}</ng2>{{l('1C')}}</div>");
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
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']);
ref.dispose();
async.done();
});
}));
});
describe('downgrade ng2 component', () => {
it('should bind properties, events', inject([AsyncTestCompleter], (async) => {
var adapter: UpgradeAdapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
ng1Module.run(($rootScope) => {
$rootScope.dataA = 'A';
$rootScope.dataB = 'B';
$rootScope.modelA = 'initModelA';
$rootScope.modelB = 'initModelB';
$rootScope.eventA = '?';
$rootScope.eventB = '?';
});
var Ng2 =
Component({
selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [
'eventA',
'eventB',
'twoWayAEmitter: twoWayAChange',
'twoWayBEmitter: twoWayBChange'
],
template: "ignore: {{ignore}}; " +
"literal: {{literal}}; interpolate: {{interpolate}}; " +
"oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; " +
"twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{onChangesCount}})"
})
.Class({
constructor: function() {
this.onChangesCount = 0;
this.ignore = '-';
this.literal = '?';
this.interpolate = '?';
this.oneWayA = '?';
this.oneWayB = '?';
this.twoWayA = '?';
this.twoWayB = '?';
this.eventA = new EventEmitter();
this.eventB = new EventEmitter();
this.twoWayAEmitter = new EventEmitter();
this.twoWayBEmitter = new EventEmitter();
},
onChanges: function(changes) {
var assert = (prop, value) => {
if (this[prop] != value) {
throw new Error(
`Expected: '${prop}' to be '${value}' but was '${this[prop]}'`);
}
};
var assertChange = (prop, value) => {
assert(prop, value);
if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`);
}
var actValue = changes[prop].currentValue;
if (actValue != value) {
throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`);
}
};
switch (this.onChangesCount++) {
case 0:
assert('ignore', '-');
assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB');
this.twoWayAEmitter.next('newA');
this.twoWayBEmitter.next('newB');
this.eventA.next('aFired');
this.eventB.next('bFired');
break;
case 1:
assertChange('twoWayA', 'newA');
break;
case 2:
assertChange('twoWayB', 'newB');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div>
<ng2 literal="Text" interpolate="Hello {{'world'}}"
bind-one-way-a="dataA" [one-way-b]="dataB"
bindon-two-way-a="modelA" [(two-way-b)]="modelB"
on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2>
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`);
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
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;");
ref.dispose();
async.done();
});
});
}));
});
describe('upgrade ng1 component', () => {
it('should bind properties, events', inject([AsyncTestCompleter], (async) => {
var adapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
var ng1 = function() {
return {
template: 'Hello {{fullName}}; A: {{dataA}}; B: {{dataB}}; | ',
scope: {fullName: '@', modelA: '=dataA', modelB: '=dataB', event: '&'},
link: function(scope) {
scope.$watch('dataB', (v) => {
if (v == 'Savkin') {
scope.dataB = 'SAVKIN';
scope.event('WORKS');
// Should not update becaus [model-a] is uni directional
scope.dataA = 'VICTOR';
}
})
}
};
};
ng1Module.directive('ng1', ng1);
var Ng2 =
Component({
selector: 'ng2',
template:
'<ng1 full-name="{{last}}, {{first}}" [model-a]="first" [(model-b)]="last" ' +
'(event)="event=$event"></ng1>' +
'<ng1 full-name="{{\'TEST\'}}" model-a="First" model-b="Last"></ng1>' +
'{{event}}-{{last}}, {{first}}',
directives: [adapter.upgradeNg1Component('ng1')]
})
.Class({
constructor: function() {
this.first = 'Victor';
this.last = 'Savkin';
this.event = '?';
}
});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
// 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");
ref.dispose();
async.done();
}, 0);
});
}));
it('should support templateUrl fetched from $httpBackend',
inject([AsyncTestCompleter], (async) => {
var adapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
ng1Module.value('$httpBackend',
(method, url, post, cbFn) => { cbFn(200, `${method}:${url}`); });
var ng1 = function() { return {templateUrl: 'url.html'}; };
ng1Module.directive('ng1', ng1);
var Ng2 = Component({
selector: 'ng2',
template: '<ng1></ng1>',
directives: [adapter.upgradeNg1Component('ng1')]
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
expect(multiTrim(document.body.textContent)).toEqual('GET:url.html');
ref.dispose();
async.done();
});
}));
it('should support templateUrl fetched from $templateCache',
inject([AsyncTestCompleter], (async) => {
var adapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
ng1Module.run(($templateCache) => $templateCache.put('url.html', 'WORKS'));
var ng1 = function() { return {templateUrl: 'url.html'}; };
ng1Module.directive('ng1', ng1);
var Ng2 = Component({
selector: 'ng2',
template: '<ng1></ng1>',
directives: [adapter.upgradeNg1Component('ng1')]
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
expect(multiTrim(document.body.textContent)).toEqual('WORKS');
ref.dispose();
async.done();
});
}));
it('should support controller with controllerAs', inject([AsyncTestCompleter], (async) => {
var adapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
var ng1 = function() {
return {
scope: true,
template:
'{{ctl.scope}}; {{ctl.isClass}}; {{ctl.hasElement}}; {{ctl.isPublished()}}',
controllerAs: 'ctl',
controller: Class({
constructor: function($scope, $element) {
(<any>this).verifyIAmAClass();
this.scope = $scope.$parent.$parent == $scope.$root ? 'scope' : 'wrong-scope';
this.hasElement = $element[0].nodeName;
this.$element = $element;
},
verifyIAmAClass: function() { this.isClass = 'isClass'; },
isPublished: function() {
return this.$element.controller('ng1') == this ? 'published' : 'not-published';
}
})
};
};
ng1Module.directive('ng1', ng1);
var Ng2 = Component({
selector: 'ng2',
template: '<ng1></ng1>',
directives: [adapter.upgradeNg1Component('ng1')]
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
expect(multiTrim(document.body.textContent))
.toEqual('scope; isClass; NG1; published');
ref.dispose();
async.done();
});
}));
it('should support bindToController', inject([AsyncTestCompleter], (async) => {
var adapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
var ng1 = function() {
return {
scope: {title: '@'},
bindToController: true,
template: '{{ctl.title}}',
controllerAs: 'ctl',
controller: Class({constructor: function() {}})
};
};
ng1Module.directive('ng1', ng1);
var Ng2 = Component({
selector: 'ng2',
template: '<ng1 title="WORKS"></ng1>',
directives: [adapter.upgradeNg1Component('ng1')]
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
expect(multiTrim(document.body.textContent)).toEqual('WORKS');
ref.dispose();
async.done();
});
}));
it('should support single require in linking fn', inject([AsyncTestCompleter], (async) => {
var adapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
var ng1 = function($rootScope) {
return {
scope: {title: '@'},
bindToController: true,
template: '{{ctl.status}}',
require: 'ng1',
controllerAs: 'ctrl',
controller: Class({constructor: function() { this.status = 'WORKS'; }}),
link: function(scope, element, attrs, linkController) {
expect(scope.$root).toEqual($rootScope);
expect(element[0].nodeName).toEqual('NG1');
expect(linkController.status).toEqual('WORKS');
scope.ctl = linkController;
}
};
};
ng1Module.directive('ng1', ng1);
var Ng2 = Component({
selector: 'ng2',
template: '<ng1></ng1>',
directives: [adapter.upgradeNg1Component('ng1')]
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
expect(multiTrim(document.body.textContent)).toEqual('WORKS');
ref.dispose();
async.done();
});
}));
it('should support array require in linking fn', inject([AsyncTestCompleter], (async) => {
var adapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []);
var parent = function() {
return {controller: Class({constructor: function() { this.parent = 'PARENT'; }})};
};
var ng1 = function() {
return {
scope: {title: '@'},
bindToController: true,
template: '{{parent.parent}}:{{ng1.status}}',
require: ['ng1', '^parent', '?^^notFound'],
controllerAs: 'ctrl',
controller: Class({constructor: function() { this.status = 'WORKS'; }}),
link: function(scope, element, attrs, linkControllers) {
expect(linkControllers[0].status).toEqual('WORKS');
expect(linkControllers[1].parent).toEqual('PARENT');
expect(linkControllers[2]).toBe(undefined);
scope.ng1 = linkControllers[0];
scope.parent = linkControllers[1];
}
};
};
ng1Module.directive('parent', parent);
ng1Module.directive('ng1', ng1);
var Ng2 = Component({
selector: 'ng2',
template: '<ng1></ng1>',
directives: [adapter.upgradeNg1Component('ng1')]
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div><parent><ng2></ng2></parent></div>`);
adapter.bootstrap(element, ['ng1'])
.ready((ref) => {
expect(multiTrim(document.body.textContent)).toEqual('PARENT:WORKS');
ref.dispose();
async.done();
});
}));
});
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('<div>'), ['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('<div>'), ['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();
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((ref) => {
expect(multiTrim(document.body.textContent))
.toEqual("ng2[ng1[Hello World!](transclude)](project)");
ref.dispose();
async.done();
});
}));
});
});
}
function multiTrim(text: string): string {
return text.replace(/\n/g, '').replace(/\s\s+/g, ' ').trim();
}
function html(html: string): Element {
var body = document.body;
body.innerHTML = html;
if (body.childNodes.length == 1 && body.firstChild instanceof HTMLElement)
return <Element>body.firstChild;
return body;
}

View File

@ -0,0 +1,6 @@
/**
* @module
* @description
* Adapter allowing AngularJS v1 and Angular v2 to run side by side in the same application.
*/
export {UpgradeAdapter, UpgradeAdapterRef} from './src/upgrade/upgrade_adapter';