feat(ngUpgrade): faster ng2->ng1 adapter by only compiling ng1 once

The adapter only compiles ng1 template. This means that we need to 
reimplement / emulate all of the ng1’s API on the HOST element.


interface IDirective {
  compile?: IDirectiveCompileFn;               // NOT SUPPORTED
  controller?: any;                            // IMPLEMENTED
  controllerAs?: string;                       // IMPLEMENTED
  bindToController?: boolean|Object;           // IMPLEMENTED
  link?: IDirectiveLinkFn | IDirectivePrePost; // IMPLEMENTED (pre-link only)
  name?: string;                               // N/A
  priority?: number;                           // NOT SUPPORTED
  replace?: boolean;                           // NOT SUPPORTED
  require?: any;                               // IMPLEMENTED
  restrict?: string;                           // WORKING
  scope?: any;                                 // IMPLEMENTED
  template?: any;                              // IMPLEMENTED
  templateUrl?: any;                           // IMPLEMENTED
  terminal?: boolean;                          // NOT SUPPORTED
  transclude?: any;                            // IMPLEMENTED
}
This commit is contained in:
Misko Hevery 2015-10-11 11:18:11 -07:00 committed by Miško Hevery
parent 059e8faae2
commit 053b7a50e1
7 changed files with 474 additions and 67 deletions

View File

@ -7,7 +7,7 @@ declare namespace angular {
run(a: any); run(a: any);
} }
interface ICompileService { interface ICompileService {
(element: Element, transclude?: Function): ILinkFn; (element: Element | NodeList | string, transclude?: Function): ILinkFn;
} }
interface ILinkFn { interface ILinkFn {
(scope: IScope, cloneAttachFn?: Function, options?: ILinkFnOptions): void (scope: IScope, cloneAttachFn?: Function, options?: ILinkFnOptions): void
@ -17,7 +17,8 @@ declare namespace angular {
futureParentElement?: Node futureParentElement?: Node
} }
interface IRootScopeService { interface IRootScopeService {
$new(): IScope; $new(isolate?: boolean): IScope;
$id: string;
$watch(expr: any, fn?: (a1?: any, a2?: any) => void); $watch(expr: any, fn?: (a1?: any, a2?: any) => void);
$apply(): any; $apply(): any;
$apply(exp: string): any; $apply(exp: string): any;
@ -29,19 +30,53 @@ declare namespace angular {
interface IScope extends IRootScopeService {} interface IScope extends IRootScopeService {}
interface IAngularBootstrapConfig {} interface IAngularBootstrapConfig {}
interface IDirective { interface IDirective {
require?: string; compile?: IDirectiveCompileFn;
controller?: any;
controllerAs?: string;
bindToController?: boolean | Object;
link?: IDirectiveLinkFn | IDirectivePrePost;
name?: string;
priority?: number;
replace?: boolean;
require?: any;
restrict?: string; restrict?: string;
scope?: {[key: string]: string}; scope?: any;
link?: {pre?: Function, post?: Function}; template?: any;
templateUrl?: any;
terminal?: boolean;
transclude?: any;
}
interface IDirectiveCompileFn {
(templateElement: IAugmentedJQuery, templateAttributes: IAttributes,
transclude: ITranscludeFunction): IDirectivePrePost;
}
interface IDirectivePrePost {
pre?: IDirectiveLinkFn;
post?: IDirectiveLinkFn;
}
interface IDirectiveLinkFn {
(scope: IScope, instanceElement: IAugmentedJQuery, instanceAttributes: IAttributes,
controller: any, transclude: ITranscludeFunction): void;
} }
interface IAttributes { interface IAttributes {
$observe(attr: string, fn: (v: string) => void); $observe(attr: string, fn: (v: string) => void);
} }
interface ITranscludeFunction {} 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;
}
interface ICloneAttachFunction {
// Let's hint but not force cloneAttachFn's signature
(clonedElement?: IAugmentedJQuery, scope?: IScope): any;
}
interface IAugmentedJQuery { interface IAugmentedJQuery {
bind(name: string, fn: () => void); bind(name: string, fn: () => void);
data(name: string, value?: any); data(name: string, value?: any);
inheritedData(name: string, value?: any);
contents(): IAugmentedJQuery; contents(): IAugmentedJQuery;
parent(): IAugmentedJQuery;
length: number; length: number;
[index: number]: Node; [index: number]: Node;
} }
@ -53,6 +88,19 @@ declare namespace angular {
} }
function element(e: Element): IAugmentedJQuery; function element(e: Element): IAugmentedJQuery;
function bootstrap(e: Element, modules: string[], config: IAngularBootstrapConfig); function bootstrap(e: Element, modules: string[], config: IAngularBootstrapConfig);
interface IHttpBackendService {
(method: string, url: string, post?: any, callback?: Function, headers?: any, timeout?: number,
withCredentials?: boolean): void;
}
interface ICacheObject {
put<T>(key: string, value?: T): T;
get(key: string): any;
}
interface ITemplateCacheService extends ICacheObject {}
interface IControllerService {
(controllerConstructor: Function, locals?: any, later?: any, ident?: any): any;
(controllerName: string, locals?: any): any;
}
namespace auto { namespace auto {
interface IInjectorService { interface IInjectorService {

View File

@ -4,10 +4,12 @@ export const NG2_INJECTOR = 'ng2.Injector';
export const NG2_PROTO_VIEW_REF_MAP = 'ng2.ProtoViewRefMap'; export const NG2_PROTO_VIEW_REF_MAP = 'ng2.ProtoViewRefMap';
export const NG2_ZONE = 'ng2.NgZone'; export const NG2_ZONE = 'ng2.NgZone';
export const NG1_REQUIRE_INJECTOR_REF = '$' + NG2_INJECTOR + 'Controller'; export const NG1_CONTROLLER = '$controller';
export const NG1_SCOPE = '$scope'; export const NG1_SCOPE = '$scope';
export const NG1_ROOT_SCOPE = '$rootScope'; export const NG1_ROOT_SCOPE = '$rootScope';
export const NG1_COMPILE = '$compile'; export const NG1_COMPILE = '$compile';
export const NG1_HTTP_BACKEND = '$httpBackend';
export const NG1_INJECTOR = '$injector'; export const NG1_INJECTOR = '$injector';
export const NG1_PARSE = '$parse'; export const NG1_PARSE = '$parse';
export const NG1_TEMPLATE_CACHE = '$templateCache';
export const REQUIRE_INJECTOR = '^' + NG2_INJECTOR; export const REQUIRE_INJECTOR = '^' + NG2_INJECTOR;

View File

@ -101,7 +101,7 @@ export class DowngradeNg2ComponentAdapter {
this.component.onChanges(inputChanges); this.component.onChanges(inputChanges);
}); });
} }
this.componentScope.$watch(() => this.changeDetector.detectChanges()); this.componentScope.$watch(() => this.changeDetector && this.changeDetector.detectChanges());
} }
projectContent() { projectContent() {

View File

@ -18,13 +18,12 @@ import {applicationCommonBindings} from 'angular2/src/core/application_ref';
import {compilerProviders} from 'angular2/src/core/compiler/compiler'; import {compilerProviders} from 'angular2/src/core/compiler/compiler';
import {getComponentInfo, ComponentInfo} from './metadata'; import {getComponentInfo, ComponentInfo} from './metadata';
import {onError} from './util'; import {onError, controllerKey} from './util';
import { import {
NG1_COMPILE, NG1_COMPILE,
NG1_INJECTOR, NG1_INJECTOR,
NG1_PARSE, NG1_PARSE,
NG1_ROOT_SCOPE, NG1_ROOT_SCOPE,
NG1_REQUIRE_INJECTOR_REF,
NG1_SCOPE, NG1_SCOPE,
NG2_APP_VIEW_MANAGER, NG2_APP_VIEW_MANAGER,
NG2_COMPILER, NG2_COMPILER,
@ -70,8 +69,10 @@ var upgradeCount: number = 0;
* 7. Whenever an adapter component is instantiated the host element is owned by the the framework * 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 * 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 * component. This implies that component bindings will always follow the semantics of the
* instantiation framework, but with Angular v2 syntax. * instantiation framework. The syntax is always that of Angular v2 syntax.
* 8. AngularJS v1 is always bootstrapped first and owns the bottom most view. * 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 * ## Example
* *
@ -81,7 +82,7 @@ var upgradeCount: number = 0;
* *
* module.directive('ng1', function() { * module.directive('ng1', function() {
* return { * return {
* scope: { title: '@' }, * scope: { title: '=' },
* template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)' * template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)'
* }; * };
* }); * });
@ -127,8 +128,8 @@ export class UpgradeAdapter {
} }
bootstrap(element: Element, modules?: any[], bootstrap(element: Element, modules?: any[],
config?: angular.IAngularBootstrapConfig): UpgradeRef { config?: angular.IAngularBootstrapConfig): UpgradeAdapterRef {
var upgrade = new UpgradeRef(); var upgrade = new UpgradeAdapterRef();
var ng1Injector: angular.auto.IInjectorService = null; var ng1Injector: angular.auto.IInjectorService = null;
var platformRef: PlatformRef = platform(); var platformRef: PlatformRef = platform();
var applicationRef: ApplicationRef = platformRef.application([ var applicationRef: ApplicationRef = platformRef.application([
@ -147,6 +148,7 @@ export class UpgradeAdapter {
var rootScope: angular.IRootScopeService; var rootScope: angular.IRootScopeService;
var protoViewRefMap: ProtoViewRefMap = {}; var protoViewRefMap: ProtoViewRefMap = {};
var ng1Module = angular.module(this.idPrefix, modules); var ng1Module = angular.module(this.idPrefix, modules);
var ng1compilePromise: Promise<any> = null;
ng1Module.value(NG2_INJECTOR, injector) ng1Module.value(NG2_INJECTOR, injector)
.value(NG2_ZONE, ngZone) .value(NG2_ZONE, ngZone)
.value(NG2_COMPILER, compiler) .value(NG2_COMPILER, compiler)
@ -176,22 +178,23 @@ export class UpgradeAdapter {
(injector: angular.auto.IInjectorService, rootScope: angular.IRootScopeService) => { (injector: angular.auto.IInjectorService, rootScope: angular.IRootScopeService) => {
ng1Injector = injector; ng1Injector = injector;
ngZone.overrideOnTurnDone(() => rootScope.$apply()); ngZone.overrideOnTurnDone(() => rootScope.$apply());
ng1compilePromise =
UpgradeNg1ComponentAdapterBuilder.resolve(this.downgradedComponents, injector); UpgradeNg1ComponentAdapterBuilder.resolve(this.downgradedComponents, injector);
} }
]); ]);
angular.element(element).data(NG1_REQUIRE_INJECTOR_REF, injector); angular.element(element).data(controllerKey(NG2_INJECTOR), injector);
ngZone.run(() => { angular.bootstrap(element, [this.idPrefix], config); }); ngZone.run(() => { angular.bootstrap(element, [this.idPrefix], config); });
this.compileNg2Components(compiler, protoViewRefMap) Promise.all([this.compileNg2Components(compiler, protoViewRefMap), ng1compilePromise])
.then((protoViewRefMap: ProtoViewRefMap) => { .then(() => {
ngZone.run(() => { ngZone.run(() => {
rootScopePrototype.$apply = original$applyFn; // restore original $apply rootScopePrototype.$apply = original$applyFn; // restore original $apply
while (delayApplyExps.length) { while (delayApplyExps.length) {
rootScope.$apply(delayApplyExps.shift()); rootScope.$apply(delayApplyExps.shift());
} }
upgrade.readyFn && upgrade.readyFn(); (<any>upgrade)._bootstrapDone(applicationRef, ng1Injector);
});
}); });
}, onError);
return upgrade; return upgrade;
} }
@ -214,7 +217,7 @@ export class UpgradeAdapter {
} }
interface ProtoViewRefMap { interface ProtoViewRefMap {
[selector: string]: ProtoViewRef [selector: string]: ProtoViewRef;
} }
function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function { function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function {
@ -246,8 +249,38 @@ function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function
return directiveFactory; return directiveFactory;
} }
export class UpgradeRef { /**
readyFn: Function; * Use `UgradeAdapterRef` to control a hybrid AngularJS v1 / Angular v2 application.
*/
export class UpgradeAdapterRef {
/* @internal */
private _readyFn: (upgradeAdapterRef?: UpgradeAdapterRef) => void = null;
ready(fn: Function) { this.readyFn = fn; } public applicationRef: ApplicationRef = null;
public ng1Injector: angular.auto.IInjectorService = null;
/* @internal */
private _bootstrapDone(applicationRef: ApplicationRef,
ng1Injector: angular.auto.IInjectorService) {
this.applicationRef = applicationRef;
this.ng1Injector = ng1Injector;
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.applicationRef.dispose();
}
} }

View File

@ -8,12 +8,20 @@ import {
SimpleChange, SimpleChange,
Type Type
} from 'angular2/angular2'; } from 'angular2/angular2';
import {NG1_COMPILE, NG1_SCOPE} from './constants'; import {
NG1_COMPILE,
NG1_SCOPE,
NG1_HTTP_BACKEND,
NG1_TEMPLATE_CACHE,
NG1_CONTROLLER
} from './constants';
import {controllerKey} from './util';
const CAMEL_CASE = /([A-Z])/g; const CAMEL_CASE = /([A-Z])/g;
const INITIAL_VALUE = { const INITIAL_VALUE = {
__UNINITIALIZED__: true __UNINITIALIZED__: true
}; };
const NOT_SUPPORTED: any = 'NOT_SUPPORTED';
export class UpgradeNg1ComponentAdapterBuilder { export class UpgradeNg1ComponentAdapterBuilder {
@ -25,6 +33,9 @@ export class UpgradeNg1ComponentAdapterBuilder {
propertyOutputs: string[] = []; propertyOutputs: string[] = [];
checkProperties: string[] = []; checkProperties: string[] = [];
propertyMap: {[name: string]: string} = {}; propertyMap: {[name: string]: string} = {};
linkFn: angular.ILinkFn = null;
directive: angular.IDirective = null;
$controller: angular.IControllerService = null;
constructor(public name: string) { constructor(public name: string) {
var selector = name.replace(CAMEL_CASE, (all, next: string) => '-' + next.toLowerCase()); var selector = name.replace(CAMEL_CASE, (all, next: string) => '-' + next.toLowerCase());
@ -33,14 +44,12 @@ export class UpgradeNg1ComponentAdapterBuilder {
Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename}) Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename})
.Class({ .Class({
constructor: [ constructor: [
new Inject(NG1_COMPILE),
new Inject(NG1_SCOPE), new Inject(NG1_SCOPE),
ElementRef, ElementRef,
function(compile: angular.ICompileService, scope: angular.IScope, function(scope: angular.IScope, elementRef: ElementRef) {
elementRef: ElementRef) { return new UpgradeNg1ComponentAdapter(
return new UpgradeNg1ComponentAdapter(compile, scope, elementRef, self.inputs, self.linkFn, scope, self.directive, elementRef, self.$controller, self.inputs,
self.outputs, self.propertyOutputs, self.outputs, self.propertyOutputs, self.checkProperties, self.propertyMap);
self.checkProperties, self.propertyMap);
} }
], ],
onChanges: function() { /* needs to be here for ng2 to properly detect it */ }, onChanges: function() { /* needs to be here for ng2 to properly detect it */ },
@ -48,13 +57,27 @@ export class UpgradeNg1ComponentAdapterBuilder {
}); });
} }
extractBindings(injector: angular.auto.IInjectorService) { extractDirective(injector: angular.auto.IInjectorService): angular.IDirective {
var directives: angular.IDirective[] = injector.get(this.name + 'Directive'); var directives: angular.IDirective[] = injector.get(this.name + 'Directive');
if (directives.length > 1) { if (directives.length > 1) {
throw new Error('Only support single directive definition for: ' + this.name); throw new Error('Only support single directive definition for: ' + this.name);
} }
var directive = directives[0]; var directive = directives[0];
var scope = directive.scope; 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') { if (typeof scope == 'object') {
for (var name in scope) { for (var name in scope) {
if ((<any>scope).hasOwnProperty(name)) { if ((<any>scope).hasOwnProperty(name)) {
@ -94,25 +117,66 @@ export class UpgradeNg1ComponentAdapterBuilder {
} }
} }
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) {
var div = document.createElement('div');
div.innerHTML = html;
return compile(div.childNodes);
}
}
static resolve(exportedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder}, static resolve(exportedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder},
injector: angular.auto.IInjectorService) { injector: angular.auto.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) { for (var name in exportedComponents) {
if ((<any>exportedComponents).hasOwnProperty(name)) { if ((<any>exportedComponents).hasOwnProperty(name)) {
var exportedComponent = exportedComponents[name]; var exportedComponent = exportedComponents[name];
exportedComponent.extractBindings(injector); 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 { class UpgradeNg1ComponentAdapter implements OnChanges, DoCheck {
componentScope: angular.IScope = null; destinationObj: any = null;
checkLastValues: any[] = []; checkLastValues: any[] = [];
constructor(compile: angular.ICompileService, scope: angular.IScope, elementRef: ElementRef, 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 inputs: string[], private outputs: string[], private propOuts: string[],
private checkProperties: string[], private propertyMap: {[key: string]: string}) { private checkProperties: string[], private propertyMap: {[key: string]: string}) {
var chailTail = scope.$$childTail; // remember where the next scope is inserted
var element: Element = elementRef.nativeElement; var element: Element = elementRef.nativeElement;
var childNodes: Node[] = []; var childNodes: Node[] = [];
var childNode; var childNode;
@ -120,11 +184,31 @@ class UpgradeNg1ComponentAdapter implements OnChanges, DoCheck {
element.removeChild(childNode); element.removeChild(childNode);
childNodes.push(childNode); childNodes.push(childNode);
} }
element.appendChild(element.ownerDocument.createElement('ng-transclude')); var componentScope = scope.$new(!!directive.scope);
compile(element)(scope, null, var $element = angular.element(element);
{parentBoundTranscludeFn: (scope, cloneAttach) => cloneAttach(childNodes)}); var controllerType = directive.controller;
// If we are first scope take it, otherwise take the next one in list. var controller: any = null;
this.componentScope = chailTail ? chailTail.$$nextSibling : scope.$$childHead; 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++) { for (var i = 0; i < inputs.length; i++) {
this[inputs[i]] = null; this[inputs[i]] = null;
@ -150,11 +234,11 @@ class UpgradeNg1ComponentAdapter implements OnChanges, DoCheck {
doCheck() { doCheck() {
var count = 0; var count = 0;
var scope = this.componentScope; var destinationObj = this.destinationObj;
var lastValues = this.checkLastValues; var lastValues = this.checkLastValues;
var checkProperties = this.checkProperties; var checkProperties = this.checkProperties;
for (var i = 0; i < checkProperties.length; i++) { for (var i = 0; i < checkProperties.length; i++) {
var value = scope[checkProperties[i]]; var value = destinationObj[checkProperties[i]];
var last = lastValues[i]; var last = lastValues[i];
if (value !== last) { if (value !== last) {
if (typeof value == 'number' && isNaN(value) && typeof last == 'number' && isNaN(last)) { if (typeof value == 'number' && isNaN(value) && typeof last == 'number' && isNaN(last)) {
@ -169,6 +253,46 @@ class UpgradeNg1ComponentAdapter implements OnChanges, DoCheck {
} }
setComponentProperty(name: string, value: any) { setComponentProperty(name: string, value: any) {
this.componentScope[this.propertyMap[name]] = value; this.destinationObj[this.propertyMap[name]] = value;
}
private resolveRequired($element: angular.IAugmentedJQuery, require: string | string[]): any {
if (!require) {
return undefined;
} else if (typeof require == 'string') {
var name: string = <string>require;
var isOptional = false;
var startParent = false;
var searchParents = false;
var ch: string;
if (name.charAt(0) == '?') {
isOptional = true;
name = name.substr(1);
}
if (name.charAt(0) == '^') {
searchParents = true;
name = name.substr(1);
}
if (name.charAt(0) == '^') {
startParent = true;
name = name.substr(1);
}
var key = controllerKey(name);
if (startParent) $element = $element.parent();
var dep = searchParents ? $element.inheritedData(key) : $element.data(key);
if (!dep && !isOptional) {
throw new Error(`Can not locate '${require}' in '${this.directive.name}'.`);
}
return dep;
} else if (require instanceof Array) {
var deps = [];
for (var i = 0; i < require.length; i++) {
deps.push(this.resolveRequired($element, require[i]));
}
return deps;
}
throw new Error(
`Directive '${this.directive.name}' require syntax unrecognized: ${this.directive.require}`);
} }
} }

View File

@ -10,3 +10,7 @@ export function onError(e: any) {
console.log(e, e.stack); console.log(e, e.stack);
throw e; throw e;
} }
export function controllerKey(name: string): string {
return '$' + name + 'Controller';
}

View File

@ -11,7 +11,7 @@ import {
xit, xit,
} from 'angular2/testing_internal'; } from 'angular2/testing_internal';
import {Component, Inject, EventEmitter} from 'angular2/angular2'; import {Component, Class, Inject, EventEmitter} from 'angular2/angular2';
import {UpgradeAdapter} from 'upgrade/upgrade'; import {UpgradeAdapter} from 'upgrade/upgrade';
export function main() { export function main() {
@ -29,8 +29,9 @@ export function main() {
var adapter: UpgradeAdapter = new UpgradeAdapter(); var adapter: UpgradeAdapter = new UpgradeAdapter();
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
adapter.bootstrap(element, ['ng1']) adapter.bootstrap(element, ['ng1'])
.ready(() => { .ready((ref) => {
expect(document.body.textContent).toEqual("ng1[NG2(~ng-content~)]"); expect(document.body.textContent).toEqual("ng1[NG2(~ng-content~)]");
ref.dispose();
async.done(); async.done();
}); });
})); }));
@ -54,8 +55,9 @@ export function main() {
var element = html("<div>{{'ng1('}}<ng2></ng2>{{')'}}</div>"); var element = html("<div>{{'ng1('}}<ng2></ng2>{{')'}}</div>");
adapter.bootstrap(element, ['ng1']) adapter.bootstrap(element, ['ng1'])
.ready(() => { .ready((ref) => {
expect(document.body.textContent).toEqual("ng1(ng2(ng1(transclude)))"); expect(document.body.textContent).toEqual("ng1(ng2(ng1(transclude)))");
ref.dispose();
async.done(); async.done();
}); });
})); }));
@ -90,16 +92,17 @@ export function main() {
var element = html("<div>{{reset(); l('1A');}}<ng2>{{l('1B')}}</ng2>{{l('1C')}}</div>"); var element = html("<div>{{reset(); l('1A');}}<ng2>{{l('1B')}}</ng2>{{l('1C')}}</div>");
adapter.bootstrap(element, ['ng1']) adapter.bootstrap(element, ['ng1'])
.ready(() => { .ready((ref) => {
expect(document.body.textContent).toEqual("1A;2A;ng1a;2B;ng1b;2C;1C;"); expect(document.body.textContent).toEqual("1A;2A;ng1a;2B;ng1b;2C;1C;");
// https://github.com/angular/angular.js/issues/12983 // https://github.com/angular/angular.js/issues/12983
expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']);
ref.dispose();
async.done(); async.done();
}); });
})); }));
}); });
describe('binding from ng1 to ng2', () => { describe('downgrade ng2 component', () => {
it('should bind properties, events', inject([AsyncTestCompleter], (async) => { it('should bind properties, events', inject([AsyncTestCompleter], (async) => {
var adapter: UpgradeAdapter = new UpgradeAdapter(); var adapter: UpgradeAdapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []); var ng1Module = angular.module('ng1', []);
@ -197,7 +200,7 @@ export function main() {
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}}; | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`); </div>`);
adapter.bootstrap(element, ['ng1']) adapter.bootstrap(element, ['ng1'])
.ready(() => { .ready((ref) => {
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toEqual( .toEqual(
"ignore: -; " + "literal: Text; interpolate: Hello world; " + "ignore: -; " + "literal: Text; interpolate: Hello world; " +
@ -210,6 +213,7 @@ export function main() {
.toEqual("ignore: -; " + "literal: Text; interpolate: Hello world; " + .toEqual("ignore: -; " + "literal: Text; interpolate: Hello world; " +
"oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | " + "oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | " +
"modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;"); "modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;");
ref.dispose();
async.done(); async.done();
}); });
}); });
@ -217,7 +221,7 @@ export function main() {
})); }));
}); });
describe('binding from ng2 to ng1', () => { describe('upgrade ng1 component', () => {
it('should bind properties, events', inject([AsyncTestCompleter], (async) => { it('should bind properties, events', inject([AsyncTestCompleter], (async) => {
var adapter = new UpgradeAdapter(); var adapter = new UpgradeAdapter();
var ng1Module = angular.module('ng1', []); var ng1Module = angular.module('ng1', []);
@ -237,7 +241,7 @@ export function main() {
} }
}) })
} }
} };
}; };
ng1Module.directive('ng1', ng1); ng1Module.directive('ng1', ng1);
var Ng2 = var Ng2 =
@ -260,17 +264,208 @@ export function main() {
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html(`<div><ng2></ng2></div>`); var element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1']) adapter.bootstrap(element, ['ng1'])
.ready(() => { .ready((ref) => {
// we need to do setTimeout, because the EventEmitter uses setTimeout to schedule // we need to do setTimeout, because the EventEmitter uses setTimeout to schedule
// events, and so without this we would not see the events processed. // events, and so without this we would not see the events processed.
setTimeout(() => { setTimeout(() => {
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toEqual( .toEqual(
"Hello SAVKIN, Victor; A: VICTOR; B: SAVKIN; | Hello TEST; A: First; B: Last; | WORKS-SAVKIN, Victor"); "Hello SAVKIN, Victor; A: VICTOR; B: SAVKIN; | Hello TEST; A: First; B: Last; | WORKS-SAVKIN, Victor");
ref.dispose();
async.done(); async.done();
}, 0); }, 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',
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'],
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('examples', () => { describe('examples', () => {
@ -280,7 +475,7 @@ export function main() {
module.directive('ng1', function() { module.directive('ng1', function() {
return { return {
scope: {title: '@'}, scope: {title: '='},
transclude: true, template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)' transclude: true, template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)'
}; };
}); });
@ -299,9 +494,10 @@ export function main() {
document.body.innerHTML = '<ng2 name="World">project</ng2>'; document.body.innerHTML = '<ng2 name="World">project</ng2>';
adapter.bootstrap(document.body, ['myExample']) adapter.bootstrap(document.body, ['myExample'])
.ready(function() { .ready((ref) => {
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toEqual("ng2[ng1[Hello World!](transclude)](project)"); .toEqual("ng2[ng1[Hello World!](transclude)](project)");
ref.dispose();
async.done(); async.done();
}); });
})); }));