chore(ngUpgrade): Move into Angular2

This is moving ngUpgrade into the main repository per #4838.
The ngUpgrade is published from the main import consistent with
https://docs.google.com/document/d/1rbVTKTYLz6p2smQNYI8h4-QN-m2PS6F3iQIDmSzn0Ww/edit#heading=h.6cxvr9awtf5r
Closes #4931
This commit is contained in:
Misko Hevery
2015-10-31 20:48:27 -07:00
parent 54f7e62c43
commit 1b78342e23
19 changed files with 183 additions and 175 deletions
+123
View File
@@ -0,0 +1,123 @@
export interface IModule {
config(fn: any): IModule;
directive(selector: string, factory: any): IModule;
controller(name: string, type: any): IModule;
factory(key: string, factoryFn: any): IModule;
value(key: string, value: any): IModule;
run(a: any): void;
}
export interface ICompileService {
(element: Element | NodeList | string, transclude?: Function): ILinkFn;
}
export interface ILinkFn {
(scope: IScope, cloneAttachFn?: Function, options?: ILinkFnOptions): void;
}
export interface ILinkFnOptions {
parentBoundTranscludeFn?: Function;
transcludeControllers?: {[key: string]: any};
futureParentElement?: Node;
}
export interface IRootScopeService {
$new(isolate?: boolean): IScope;
$id: string;
$watch(expr: any, fn?: (a1?: any, a2?: any) => void): Function;
$apply(): any;
$apply(exp: string): any;
$apply(exp: Function): any;
$$childTail: IScope;
$$childHead: IScope;
$$nextSibling: IScope;
}
export interface IScope extends IRootScopeService {}
export interface IAngularBootstrapConfig {}
export interface IDirective {
compile?: IDirectiveCompileFn;
controller?: any;
controllerAs?: string;
bindToController?: boolean | Object;
link?: IDirectiveLinkFn | IDirectivePrePost;
name?: string;
priority?: number;
replace?: boolean;
require?: any;
restrict?: string;
scope?: any;
template?: any;
templateUrl?: any;
terminal?: boolean;
transclude?: any;
}
export interface IDirectiveCompileFn {
(templateElement: IAugmentedJQuery, templateAttributes: IAttributes,
transclude: ITranscludeFunction): IDirectivePrePost;
}
export interface IDirectivePrePost {
pre?: IDirectiveLinkFn;
post?: IDirectiveLinkFn;
}
export interface IDirectiveLinkFn {
(scope: IScope, instanceElement: IAugmentedJQuery, instanceAttributes: IAttributes,
controller: any, transclude: ITranscludeFunction): void;
}
export interface IAttributes { $observe(attr: string, fn: (v: string) => void): void; }
export interface ITranscludeFunction {
// If the scope is provided, then the cloneAttachFn must be as well.
(scope: IScope, cloneAttachFn: ICloneAttachFunction): IAugmentedJQuery;
// If one argument is provided, then it's assumed to be the cloneAttachFn.
(cloneAttachFn?: ICloneAttachFunction): IAugmentedJQuery;
}
export interface ICloneAttachFunction {
// Let's hint but not force cloneAttachFn's signature
(clonedElement?: IAugmentedJQuery, scope?: IScope): any;
}
export interface IAugmentedJQuery {
bind(name: string, fn: () => void): void;
data(name: string, value?: any): any;
inheritedData(name: string, value?: any): any;
contents(): IAugmentedJQuery;
parent(): IAugmentedJQuery;
length: number;
[index: number]: Node;
}
export interface IParseService { (expression: string): ICompiledExpression; }
export interface ICompiledExpression { assign(context: any, value: any): any; }
export interface IHttpBackendService {
(method: string, url: string, post?: any, callback?: Function, headers?: any, timeout?: number,
withCredentials?: boolean): void;
}
export interface ICacheObject {
put<T>(key: string, value?: T): T;
get(key: string): any;
}
export interface ITemplateCacheService extends ICacheObject {}
export interface IControllerService {
(controllerConstructor: Function, locals?: any, later?: any, ident?: any): any;
(controllerName: string, locals?: any): any;
}
export interface IInjectorService { get(key: string): any; }
function noNg() {
throw new Error('AngularJS v1.x is not loaded!');
}
var angular: {
bootstrap: (e: Element, modules: string[], config: IAngularBootstrapConfig) => void,
module: (prefix: string, dependencies?: string[]) => IModule,
element: (e: Element) => IAugmentedJQuery,
version: {major: number}
} = <any>{bootstrap: noNg, module: noNg, element: noNg, version: noNg};
try {
if (window.hasOwnProperty('angular')) {
angular = (<any>window).angular;
}
} catch (e) {
// ignore in CJS mode.
}
export var bootstrap = angular.bootstrap;
export var module = angular.module;
export var element = angular.element;
export var version = angular.version;
+15
View File
@@ -0,0 +1,15 @@
export const NG2_APP_VIEW_MANAGER = 'ng2.AppViewManager';
export const NG2_COMPILER = 'ng2.Compiler';
export const NG2_INJECTOR = 'ng2.Injector';
export const NG2_PROTO_VIEW_REF_MAP = 'ng2.ProtoViewRefMap';
export const NG2_ZONE = 'ng2.NgZone';
export const NG1_CONTROLLER = '$controller';
export const NG1_SCOPE = '$scope';
export const NG1_ROOT_SCOPE = '$rootScope';
export const NG1_COMPILE = '$compile';
export const NG1_HTTP_BACKEND = '$httpBackend';
export const NG1_INJECTOR = '$injector';
export const NG1_PARSE = '$parse';
export const NG1_TEMPLATE_CACHE = '$templateCache';
export const REQUIRE_INJECTOR = '^' + NG2_INJECTOR;
@@ -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
View File
@@ -0,0 +1,62 @@
import {Type, ComponentMetadata, DirectiveResolver, DirectiveMetadata} from 'angular2/angular2';
import {stringify} from './util';
var COMPONENT_SELECTOR = /^[\w|-]*$/;
var SKEWER_CASE = /-(\w)/g;
var directiveResolver = new DirectiveResolver();
export interface AttrProp {
prop: string;
attr: string;
bracketAttr: string;
bracketParenAttr: string;
parenAttr: string;
onAttr: string;
bindAttr: string;
bindonAttr: string;
}
export interface ComponentInfo {
type: Type;
selector: string;
inputs: AttrProp[];
outputs: AttrProp[];
}
export function getComponentInfo(type: Type): ComponentInfo {
var resolvedMetadata: DirectiveMetadata = directiveResolver.resolve(type);
var selector = resolvedMetadata.selector;
if (!selector.match(COMPONENT_SELECTOR)) {
throw new Error('Only selectors matching element names are supported, got: ' + selector);
}
var selector = selector.replace(SKEWER_CASE, (all, letter: string) => letter.toUpperCase());
return {
type: type,
selector: selector,
inputs: parseFields(resolvedMetadata.inputs),
outputs: parseFields(resolvedMetadata.outputs)
};
}
export function parseFields(names: string[]): AttrProp[] {
var attrProps: AttrProp[] = [];
if (names) {
for (var i = 0; i < names.length; i++) {
var parts = names[i].split(':');
var prop = parts[0].trim();
var attr = (parts[1] || parts[0]).trim();
var capitalAttr = attr.charAt(0).toUpperCase() + attr.substr(1);
attrProps.push(<AttrProp>{
prop: prop,
attr: attr,
bracketAttr: `[${attr}]`,
parenAttr: `(${attr})`,
bracketParenAttr: `[(${attr})]`,
onAttr: `on${capitalAttr}`,
bindAttr: `bind${capitalAttr}`,
bindonAttr: `bindon${capitalAttr}`
});
}
}
return attrProps;
}
File diff suppressed because it is too large Load Diff
@@ -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
View File
@@ -0,0 +1,16 @@
export function stringify(obj: any): string {
if (typeof obj == 'function') return obj.name || obj.toString();
return '' + obj;
}
export function onError(e: any) {
// TODO: (misko): We seem to not have a stack trace here!
console.log(e, e.stack);
throw e;
}
export function controllerKey(name: string): string {
return '$' + name + 'Controller';
}