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:
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';
|
||||
}
|
Reference in New Issue
Block a user