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:
parent
059e8faae2
commit
053b7a50e1
|
@ -7,7 +7,7 @@ declare namespace angular {
|
|||
run(a: any);
|
||||
}
|
||||
interface ICompileService {
|
||||
(element: Element, transclude?: Function): ILinkFn;
|
||||
(element: Element | NodeList | string, transclude?: Function): ILinkFn;
|
||||
}
|
||||
interface ILinkFn {
|
||||
(scope: IScope, cloneAttachFn?: Function, options?: ILinkFnOptions): void
|
||||
|
@ -17,7 +17,8 @@ declare namespace angular {
|
|||
futureParentElement?: Node
|
||||
}
|
||||
interface IRootScopeService {
|
||||
$new(): IScope;
|
||||
$new(isolate?: boolean): IScope;
|
||||
$id: string;
|
||||
$watch(expr: any, fn?: (a1?: any, a2?: any) => void);
|
||||
$apply(): any;
|
||||
$apply(exp: string): any;
|
||||
|
@ -29,19 +30,53 @@ declare namespace angular {
|
|||
interface IScope extends IRootScopeService {}
|
||||
interface IAngularBootstrapConfig {}
|
||||
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;
|
||||
scope?: {[key: string]: string};
|
||||
link?: {pre?: Function, post?: Function};
|
||||
scope?: any;
|
||||
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 {
|
||||
$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 {
|
||||
bind(name: string, fn: () => void);
|
||||
data(name: string, value?: any);
|
||||
inheritedData(name: string, value?: any);
|
||||
contents(): IAugmentedJQuery;
|
||||
parent(): IAugmentedJQuery;
|
||||
length: number;
|
||||
[index: number]: Node;
|
||||
}
|
||||
|
@ -53,6 +88,19 @@ declare namespace angular {
|
|||
}
|
||||
function element(e: Element): IAugmentedJQuery;
|
||||
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 {
|
||||
interface IInjectorService {
|
||||
|
|
|
@ -4,10 +4,12 @@ export const NG2_INJECTOR = 'ng2.Injector';
|
|||
export const NG2_PROTO_VIEW_REF_MAP = 'ng2.ProtoViewRefMap';
|
||||
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_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;
|
||||
|
|
|
@ -101,7 +101,7 @@ export class DowngradeNg2ComponentAdapter {
|
|||
this.component.onChanges(inputChanges);
|
||||
});
|
||||
}
|
||||
this.componentScope.$watch(() => this.changeDetector.detectChanges());
|
||||
this.componentScope.$watch(() => this.changeDetector && this.changeDetector.detectChanges());
|
||||
}
|
||||
|
||||
projectContent() {
|
||||
|
|
|
@ -18,13 +18,12 @@ import {applicationCommonBindings} from 'angular2/src/core/application_ref';
|
|||
import {compilerProviders} from 'angular2/src/core/compiler/compiler';
|
||||
|
||||
import {getComponentInfo, ComponentInfo} from './metadata';
|
||||
import {onError} from './util';
|
||||
import {onError, controllerKey} from './util';
|
||||
import {
|
||||
NG1_COMPILE,
|
||||
NG1_INJECTOR,
|
||||
NG1_PARSE,
|
||||
NG1_ROOT_SCOPE,
|
||||
NG1_REQUIRE_INJECTOR_REF,
|
||||
NG1_SCOPE,
|
||||
NG2_APP_VIEW_MANAGER,
|
||||
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
|
||||
* 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, 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.
|
||||
* 9. The new application is running in Angular v2 zone, and therefore it no longer needs calls to
|
||||
* `$apply()`.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
|
@ -81,7 +82,7 @@ var upgradeCount: number = 0;
|
|||
*
|
||||
* module.directive('ng1', function() {
|
||||
* return {
|
||||
* scope: { title: '@' },
|
||||
* scope: { title: '=' },
|
||||
* template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)'
|
||||
* };
|
||||
* });
|
||||
|
@ -127,8 +128,8 @@ export class UpgradeAdapter {
|
|||
}
|
||||
|
||||
bootstrap(element: Element, modules?: any[],
|
||||
config?: angular.IAngularBootstrapConfig): UpgradeRef {
|
||||
var upgrade = new UpgradeRef();
|
||||
config?: angular.IAngularBootstrapConfig): UpgradeAdapterRef {
|
||||
var upgrade = new UpgradeAdapterRef();
|
||||
var ng1Injector: angular.auto.IInjectorService = null;
|
||||
var platformRef: PlatformRef = platform();
|
||||
var applicationRef: ApplicationRef = platformRef.application([
|
||||
|
@ -147,6 +148,7 @@ export class UpgradeAdapter {
|
|||
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)
|
||||
|
@ -176,22 +178,23 @@ export class UpgradeAdapter {
|
|||
(injector: angular.auto.IInjectorService, rootScope: angular.IRootScopeService) => {
|
||||
ng1Injector = injector;
|
||||
ngZone.overrideOnTurnDone(() => rootScope.$apply());
|
||||
UpgradeNg1ComponentAdapterBuilder.resolve(this.downgradedComponents, injector);
|
||||
ng1compilePromise =
|
||||
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); });
|
||||
this.compileNg2Components(compiler, protoViewRefMap)
|
||||
.then((protoViewRefMap: ProtoViewRefMap) => {
|
||||
Promise.all([this.compileNg2Components(compiler, protoViewRefMap), ng1compilePromise])
|
||||
.then(() => {
|
||||
ngZone.run(() => {
|
||||
rootScopePrototype.$apply = original$applyFn; // restore original $apply
|
||||
while (delayApplyExps.length) {
|
||||
rootScope.$apply(delayApplyExps.shift());
|
||||
}
|
||||
upgrade.readyFn && upgrade.readyFn();
|
||||
(<any>upgrade)._bootstrapDone(applicationRef, ng1Injector);
|
||||
});
|
||||
});
|
||||
}, onError);
|
||||
return upgrade;
|
||||
}
|
||||
|
||||
|
@ -214,7 +217,7 @@ export class UpgradeAdapter {
|
|||
}
|
||||
|
||||
interface ProtoViewRefMap {
|
||||
[selector: string]: ProtoViewRef
|
||||
[selector: string]: ProtoViewRef;
|
||||
}
|
||||
|
||||
function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function {
|
||||
|
@ -246,8 +249,38 @@ function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,20 @@ import {
|
|||
SimpleChange,
|
||||
Type
|
||||
} 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 INITIAL_VALUE = {
|
||||
__UNINITIALIZED__: true
|
||||
};
|
||||
const NOT_SUPPORTED: any = 'NOT_SUPPORTED';
|
||||
|
||||
|
||||
export class UpgradeNg1ComponentAdapterBuilder {
|
||||
|
@ -25,6 +33,9 @@ export class UpgradeNg1ComponentAdapterBuilder {
|
|||
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());
|
||||
|
@ -33,14 +44,12 @@ export class UpgradeNg1ComponentAdapterBuilder {
|
|||
Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename})
|
||||
.Class({
|
||||
constructor: [
|
||||
new Inject(NG1_COMPILE),
|
||||
new Inject(NG1_SCOPE),
|
||||
ElementRef,
|
||||
function(compile: angular.ICompileService, scope: angular.IScope,
|
||||
elementRef: ElementRef) {
|
||||
return new UpgradeNg1ComponentAdapter(compile, scope, elementRef, self.inputs,
|
||||
self.outputs, self.propertyOutputs,
|
||||
self.checkProperties, self.propertyMap);
|
||||
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 */ },
|
||||
|
@ -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');
|
||||
if (directives.length > 1) {
|
||||
throw new Error('Only support single directive definition for: ' + this.name);
|
||||
}
|
||||
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') {
|
||||
for (var name in scope) {
|
||||
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},
|
||||
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) {
|
||||
if ((<any>exportedComponents).hasOwnProperty(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 {
|
||||
componentScope: angular.IScope = null;
|
||||
destinationObj: any = null;
|
||||
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 checkProperties: string[], private propertyMap: {[key: string]: string}) {
|
||||
var chailTail = scope.$$childTail; // remember where the next scope is inserted
|
||||
var element: Element = elementRef.nativeElement;
|
||||
var childNodes: Node[] = [];
|
||||
var childNode;
|
||||
|
@ -120,11 +184,31 @@ class UpgradeNg1ComponentAdapter implements OnChanges, DoCheck {
|
|||
element.removeChild(childNode);
|
||||
childNodes.push(childNode);
|
||||
}
|
||||
element.appendChild(element.ownerDocument.createElement('ng-transclude'));
|
||||
compile(element)(scope, null,
|
||||
{parentBoundTranscludeFn: (scope, cloneAttach) => cloneAttach(childNodes)});
|
||||
// If we are first scope take it, otherwise take the next one in list.
|
||||
this.componentScope = chailTail ? chailTail.$$nextSibling : scope.$$childHead;
|
||||
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;
|
||||
|
@ -150,11 +234,11 @@ class UpgradeNg1ComponentAdapter implements OnChanges, DoCheck {
|
|||
|
||||
doCheck() {
|
||||
var count = 0;
|
||||
var scope = this.componentScope;
|
||||
var destinationObj = this.destinationObj;
|
||||
var lastValues = this.checkLastValues;
|
||||
var checkProperties = this.checkProperties;
|
||||
for (var i = 0; i < checkProperties.length; i++) {
|
||||
var value = scope[checkProperties[i]];
|
||||
var value = destinationObj[checkProperties[i]];
|
||||
var last = lastValues[i];
|
||||
if (value !== 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) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,3 +10,7 @@ export function onError(e: any) {
|
|||
console.log(e, e.stack);
|
||||
throw e;
|
||||
}
|
||||
|
||||
export function controllerKey(name: string): string {
|
||||
return '$' + name + 'Controller';
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
xit,
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {Component, Inject, EventEmitter} from 'angular2/angular2';
|
||||
import {Component, Class, Inject, EventEmitter} from 'angular2/angular2';
|
||||
import {UpgradeAdapter} from 'upgrade/upgrade';
|
||||
|
||||
export function main() {
|
||||
|
@ -29,8 +29,9 @@ export function main() {
|
|||
var adapter: UpgradeAdapter = new UpgradeAdapter();
|
||||
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
|
||||
adapter.bootstrap(element, ['ng1'])
|
||||
.ready(() => {
|
||||
.ready((ref) => {
|
||||
expect(document.body.textContent).toEqual("ng1[NG2(~ng-content~)]");
|
||||
ref.dispose();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -54,8 +55,9 @@ export function main() {
|
|||
var element = html("<div>{{'ng1('}}<ng2></ng2>{{')'}}</div>");
|
||||
|
||||
adapter.bootstrap(element, ['ng1'])
|
||||
.ready(() => {
|
||||
.ready((ref) => {
|
||||
expect(document.body.textContent).toEqual("ng1(ng2(ng1(transclude)))");
|
||||
ref.dispose();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -90,16 +92,17 @@ export function main() {
|
|||
|
||||
var element = html("<div>{{reset(); l('1A');}}<ng2>{{l('1B')}}</ng2>{{l('1C')}}</div>");
|
||||
adapter.bootstrap(element, ['ng1'])
|
||||
.ready(() => {
|
||||
.ready((ref) => {
|
||||
expect(document.body.textContent).toEqual("1A;2A;ng1a;2B;ng1b;2C;1C;");
|
||||
// https://github.com/angular/angular.js/issues/12983
|
||||
expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']);
|
||||
ref.dispose();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('binding from ng1 to ng2', () => {
|
||||
describe('downgrade ng2 component', () => {
|
||||
it('should bind properties, events', inject([AsyncTestCompleter], (async) => {
|
||||
var adapter: UpgradeAdapter = new UpgradeAdapter();
|
||||
var ng1Module = angular.module('ng1', []);
|
||||
|
@ -197,7 +200,7 @@ export function main() {
|
|||
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
|
||||
</div>`);
|
||||
adapter.bootstrap(element, ['ng1'])
|
||||
.ready(() => {
|
||||
.ready((ref) => {
|
||||
expect(multiTrim(document.body.textContent))
|
||||
.toEqual(
|
||||
"ignore: -; " + "literal: Text; interpolate: Hello world; " +
|
||||
|
@ -210,6 +213,7 @@ export function main() {
|
|||
.toEqual("ignore: -; " + "literal: Text; interpolate: Hello world; " +
|
||||
"oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | " +
|
||||
"modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;");
|
||||
ref.dispose();
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
|
@ -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) => {
|
||||
var adapter = new UpgradeAdapter();
|
||||
var ng1Module = angular.module('ng1', []);
|
||||
|
@ -225,19 +229,19 @@ export function main() {
|
|||
var ng1 = function() {
|
||||
return {
|
||||
template: 'Hello {{fullName}}; A: {{dataA}}; B: {{dataB}}; | ',
|
||||
scope: {fullName: '@', modelA: '=dataA', modelB: '=dataB', event: '&'},
|
||||
link: function(scope) {
|
||||
scope.$watch('dataB', (v) => {
|
||||
if (v == 'Savkin') {
|
||||
scope.dataB = 'SAVKIN';
|
||||
scope.event('WORKS');
|
||||
scope: {fullName: '@', modelA: '=dataA', modelB: '=dataB', event: '&'},
|
||||
link: function(scope) {
|
||||
scope.$watch('dataB', (v) => {
|
||||
if (v == 'Savkin') {
|
||||
scope.dataB = 'SAVKIN';
|
||||
scope.event('WORKS');
|
||||
|
||||
// Should not update becaus [model-a] is uni directional
|
||||
scope.dataA = 'VICTOR';
|
||||
}
|
||||
})
|
||||
// Should not update becaus [model-a] is uni directional
|
||||
scope.dataA = 'VICTOR';
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
};
|
||||
ng1Module.directive('ng1', ng1);
|
||||
var Ng2 =
|
||||
|
@ -260,17 +264,208 @@ export function main() {
|
|||
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
|
||||
var element = html(`<div><ng2></ng2></div>`);
|
||||
adapter.bootstrap(element, ['ng1'])
|
||||
.ready(() => {
|
||||
.ready((ref) => {
|
||||
// we need to do setTimeout, because the EventEmitter uses setTimeout to schedule
|
||||
// events, and so without this we would not see the events processed.
|
||||
setTimeout(() => {
|
||||
expect(multiTrim(document.body.textContent))
|
||||
.toEqual(
|
||||
"Hello SAVKIN, Victor; A: VICTOR; B: SAVKIN; | Hello TEST; A: First; B: Last; | WORKS-SAVKIN, Victor");
|
||||
ref.dispose();
|
||||
async.done();
|
||||
}, 0);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support templateUrl fetched from $httpBackend',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
var adapter = new UpgradeAdapter();
|
||||
var ng1Module = angular.module('ng1', []);
|
||||
ng1Module.value('$httpBackend',
|
||||
(method, url, post, cbFn) => { cbFn(200, `${method}:${url}`); });
|
||||
|
||||
var ng1 = function() { return {templateUrl: 'url.html'}; };
|
||||
ng1Module.directive('ng1', ng1);
|
||||
var Ng2 = Component({
|
||||
selector: 'ng2',
|
||||
template: '<ng1></ng1>',
|
||||
directives: [adapter.upgradeNg1Component('ng1')]
|
||||
}).Class({constructor: function() {}});
|
||||
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
|
||||
var element = html(`<div><ng2></ng2></div>`);
|
||||
adapter.bootstrap(element, ['ng1'])
|
||||
.ready((ref) => {
|
||||
expect(multiTrim(document.body.textContent)).toEqual('GET:url.html');
|
||||
ref.dispose();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support templateUrl fetched from $templateCache',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
var adapter = new UpgradeAdapter();
|
||||
var ng1Module = angular.module('ng1', []);
|
||||
ng1Module.run(($templateCache) => $templateCache.put('url.html', 'WORKS'));
|
||||
|
||||
var ng1 = function() { return {templateUrl: 'url.html'}; };
|
||||
ng1Module.directive('ng1', ng1);
|
||||
var Ng2 = Component({
|
||||
selector: 'ng2',
|
||||
template: '<ng1></ng1>',
|
||||
directives: [adapter.upgradeNg1Component('ng1')]
|
||||
}).Class({constructor: function() {}});
|
||||
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
|
||||
var element = html(`<div><ng2></ng2></div>`);
|
||||
adapter.bootstrap(element, ['ng1'])
|
||||
.ready((ref) => {
|
||||
expect(multiTrim(document.body.textContent)).toEqual('WORKS');
|
||||
ref.dispose();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support controller with controllerAs', inject([AsyncTestCompleter], (async) => {
|
||||
var adapter = new UpgradeAdapter();
|
||||
var ng1Module = angular.module('ng1', []);
|
||||
|
||||
var ng1 = function() {
|
||||
return {
|
||||
scope: true,
|
||||
template:
|
||||
'{{ctl.scope}}; {{ctl.isClass}}; {{ctl.hasElement}}; {{ctl.isPublished()}}',
|
||||
controllerAs: 'ctl',
|
||||
controller: Class({
|
||||
constructor: function($scope, $element) {
|
||||
(<any>this).verifyIAmAClass();
|
||||
this.scope = $scope.$parent.$parent == $scope.$root ? 'scope' : 'wrong-scope';
|
||||
this.hasElement = $element[0].nodeName;
|
||||
this.$element = $element;
|
||||
},
|
||||
verifyIAmAClass: function() { this.isClass = 'isClass'; },
|
||||
isPublished: function() {
|
||||
return this.$element.controller('ng1') == this ? 'published' : 'not-published';
|
||||
}
|
||||
})
|
||||
};
|
||||
};
|
||||
ng1Module.directive('ng1', ng1);
|
||||
var Ng2 = Component({
|
||||
selector: 'ng2',
|
||||
template: '<ng1></ng1>',
|
||||
directives: [adapter.upgradeNg1Component('ng1')]
|
||||
}).Class({constructor: function() {}});
|
||||
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
|
||||
var element = html(`<div><ng2></ng2></div>`);
|
||||
adapter.bootstrap(element, ['ng1'])
|
||||
.ready((ref) => {
|
||||
expect(multiTrim(document.body.textContent))
|
||||
.toEqual('scope; isClass; NG1; published');
|
||||
ref.dispose();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support bindToController', inject([AsyncTestCompleter], (async) => {
|
||||
var adapter = new UpgradeAdapter();
|
||||
var ng1Module = angular.module('ng1', []);
|
||||
|
||||
var ng1 = function() {
|
||||
return {
|
||||
scope: {title: '@'},
|
||||
bindToController: true, template: '{{ctl.title}}',
|
||||
controllerAs: 'ctl',
|
||||
controller: Class({constructor: function() {}})
|
||||
};
|
||||
};
|
||||
ng1Module.directive('ng1', ng1);
|
||||
var Ng2 = Component({
|
||||
selector: 'ng2',
|
||||
template: '<ng1 title="WORKS"></ng1>',
|
||||
directives: [adapter.upgradeNg1Component('ng1')]
|
||||
}).Class({constructor: function() {}});
|
||||
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
|
||||
var element = html(`<div><ng2></ng2></div>`);
|
||||
adapter.bootstrap(element, ['ng1'])
|
||||
.ready((ref) => {
|
||||
expect(multiTrim(document.body.textContent)).toEqual('WORKS');
|
||||
ref.dispose();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support single require in linking fn', inject([AsyncTestCompleter], (async) => {
|
||||
var adapter = new UpgradeAdapter();
|
||||
var ng1Module = angular.module('ng1', []);
|
||||
|
||||
var ng1 = function($rootScope) {
|
||||
return {
|
||||
scope: {title: '@'},
|
||||
bindToController: true, template: '{{ctl.status}}',
|
||||
require: 'ng1',
|
||||
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', () => {
|
||||
|
@ -280,7 +475,7 @@ export function main() {
|
|||
|
||||
module.directive('ng1', function() {
|
||||
return {
|
||||
scope: {title: '@'},
|
||||
scope: {title: '='},
|
||||
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>';
|
||||
|
||||
adapter.bootstrap(document.body, ['myExample'])
|
||||
.ready(function() {
|
||||
.ready((ref) => {
|
||||
expect(multiTrim(document.body.textContent))
|
||||
.toEqual("ng2[ng1[Hello World!](transclude)](project)");
|
||||
ref.dispose();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
|
Loading…
Reference in New Issue