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:
@ -2,3 +2,4 @@ export * from './core';
|
||||
export * from './profile';
|
||||
export * from './lifecycle_hooks';
|
||||
export * from './bootstrap';
|
||||
export * from './upgrade';
|
||||
|
123
modules/angular2/src/upgrade/angular_js.ts
Normal file
123
modules/angular2/src/upgrade/angular_js.ts
Normal 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;
|
15
modules/angular2/src/upgrade/constants.ts
Normal file
15
modules/angular2/src/upgrade/constants.ts
Normal 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;
|
171
modules/angular2/src/upgrade/downgrade_ng2_adapter.ts
Normal file
171
modules/angular2/src/upgrade/downgrade_ng2_adapter.ts
Normal 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; }
|
||||
}
|
62
modules/angular2/src/upgrade/metadata.ts
Normal file
62
modules/angular2/src/upgrade/metadata.ts
Normal 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;
|
||||
}
|
563
modules/angular2/src/upgrade/upgrade_adapter.ts
Normal file
563
modules/angular2/src/upgrade/upgrade_adapter.ts
Normal 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();
|
||||
}
|
||||
}
|
299
modules/angular2/src/upgrade/upgrade_ng1_adapter.ts
Normal file
299
modules/angular2/src/upgrade/upgrade_ng1_adapter.ts
Normal 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}`);
|
||||
}
|
||||
}
|
16
modules/angular2/src/upgrade/util.ts
Normal file
16
modules/angular2/src/upgrade/util.ts
Normal 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';
|
||||
}
|
@ -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');
|
||||
|
81
modules/angular2/test/upgrade/metadata_spec.ts
Normal file
81
modules/angular2/test/upgrade/metadata_spec.ts
Normal 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 {}
|
562
modules/angular2/test/upgrade/upgrade_spec.ts
Normal file
562
modules/angular2/test/upgrade/upgrade_spec.ts
Normal 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;
|
||||
}
|
6
modules/angular2/upgrade.ts
Normal file
6
modules/angular2/upgrade.ts
Normal 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';
|
Reference in New Issue
Block a user