diff --git a/karma-js.conf.js b/karma-js.conf.js index 443bf417df..67454bef7a 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -23,7 +23,7 @@ module.exports = function(config) { 'node_modules/core-js/client/core.js', // include Angular v1 for upgrade module testing - 'node_modules/angular/angular.min.js', + 'node_modules/angular/angular.js', 'node_modules/zone.js/dist/zone.js', 'node_modules/zone.js/dist/long-stack-trace-zone.js', 'node_modules/zone.js/dist/proxy.js', 'node_modules/zone.js/dist/sync-test.js', diff --git a/modules/@angular/upgrade/index.ts b/modules/@angular/upgrade/index.ts index a2c3215212..cc37fbd7fe 100644 --- a/modules/@angular/upgrade/index.ts +++ b/modules/@angular/upgrade/index.ts @@ -12,5 +12,5 @@ * Entry point for all public APIs of the upgrade package. */ export * from './src/upgrade'; - +export * from './src/aot'; // This file only reexports content of the `src` folder. Keep it that way. diff --git a/modules/@angular/upgrade/src/angular_js.ts b/modules/@angular/upgrade/src/angular_js.ts index 86c7c86243..43d6799e25 100644 --- a/modules/@angular/upgrade/src/angular_js.ts +++ b/modules/@angular/upgrade/src/angular_js.ts @@ -6,20 +6,28 @@ * found in the LICENSE file at https://angular.io/license */ +export type Ng1Token = string; + +export interface IAnnotatedFunction extends Function { $inject?: Ng1Token[]; } + +export type IInjectable = (Ng1Token | Function)[] | IAnnotatedFunction; + export interface IModule { - config(fn: any): IModule; - directive(selector: string, factory: any): IModule; + name: string; + requires: (string|IInjectable)[]; + config(fn: IInjectable): IModule; + directive(selector: string, factory: IInjectable): IModule; component(selector: string, component: IComponent): IModule; - controller(name: string, type: any): IModule; - factory(key: string, factoryFn: any): IModule; - value(key: string, value: any): IModule; - run(a: any): void; + controller(name: string, type: IInjectable): IModule; + factory(key: Ng1Token, factoryFn: IInjectable): IModule; + value(key: Ng1Token, value: any): IModule; + run(a: IInjectable): IModule; } export interface ICompileService { (element: Element|NodeList|string, transclude?: Function): ILinkFn; } export interface ILinkFn { - (scope: IScope, cloneAttachFn?: Function, options?: ILinkFnOptions): void; + (scope: IScope, cloneAttachFn?: ICloneAttachFunction, options?: ILinkFnOptions): IAugmentedJQuery; } export interface ILinkFnOptions { parentBoundTranscludeFn?: Function; @@ -29,35 +37,42 @@ export interface ILinkFnOptions { export interface IRootScopeService { $new(isolate?: boolean): IScope; $id: string; + $parent: IScope; + $root: IScope; $watch(expr: any, fn?: (a1?: any, a2?: any) => void): Function; $destroy(): any; $apply(): any; $apply(exp: string): any; $apply(exp: Function): any; $evalAsync(): any; + $on(event: string, fn?: (event?: any, ...args: any[]) => void): Function; $$childTail: IScope; $$childHead: IScope; $$nextSibling: IScope; + [key: string]: any; } export interface IScope extends IRootScopeService {} -export interface IAngularBootstrapConfig {} +; +export interface IAngularBootstrapConfig { strictDi?: boolean; } export interface IDirective { compile?: IDirectiveCompileFn; - controller?: any; + controller?: IController; controllerAs?: string; - bindToController?: boolean|Object; + bindToController?: boolean|{[key: string]: string}; link?: IDirectiveLinkFn|IDirectivePrePost; name?: string; priority?: number; replace?: boolean; - require?: any; + require?: DirectiveRequireProperty; restrict?: string; - scope?: any; - template?: any; - templateUrl?: any; + scope?: boolean|{[key: string]: string}; + template?: string|Function; + templateUrl?: string|Function; + templateNamespace?: string; terminal?: boolean; - transclude?: any; + transclude?: boolean|'element'|{[key: string]: string}; } +export type DirectiveRequireProperty = Ng1Token[] | Ng1Token | {[key: string]: Ng1Token}; export interface IDirectiveCompileFn { (templateElement: IAugmentedJQuery, templateAttributes: IAttributes, transclude: ITranscludeFunction): IDirectivePrePost; @@ -71,13 +86,13 @@ export interface IDirectiveLinkFn { controller: any, transclude: ITranscludeFunction): void; } export interface IComponent { - bindings?: Object; - controller?: any; + bindings?: {[key: string]: string}; + controller?: string|IInjectable; controllerAs?: string; - require?: any; - template?: any; - templateUrl?: any; - transclude?: any; + require?: DirectiveRequireProperty; + template?: string|Function; + templateUrl?: string|Function; + transclude?: boolean; } export interface IAttributes { $observe(attr: string, fn: (v: string) => void): void; } export interface ITranscludeFunction { @@ -90,14 +105,25 @@ 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 type IAugmentedJQuery = Node[] & { + bind?: (name: string, fn: () => void) => void; + data?: (name: string, value?: any) => any; + inheritedData?: (name: string, value?: any) => any; + contents?: () => IAugmentedJQuery; + parent?: () => IAugmentedJQuery; + empty?: () => void; + append?: (content: IAugmentedJQuery | string) => IAugmentedJQuery; + controller?: (name: string) => any; + isolateScope?: () => IScope; +}; +export interface IProvider { $get: IInjectable; } +export interface IProvideService { + provider(token: Ng1Token, provider: IProvider): IProvider; + factory(token: Ng1Token, factory: IInjectable): IProvider; + service(token: Ng1Token, type: IInjectable): IProvider; + value(token: Ng1Token, value: any): IProvider; + constant(token: Ng1Token, value: any): void; + decorator(token: Ng1Token, factory: IInjectable): void; } export interface IParseService { (expression: string): ICompiledExpression; } export interface ICompiledExpression { assign(context: any, value: any): any; } @@ -110,8 +136,9 @@ export interface ICacheObject { get(key: string): any; } export interface ITemplateCacheService extends ICacheObject {} +export type IController = string | IInjectable; export interface IControllerService { - (controllerConstructor: Function, locals?: any, later?: any, ident?: any): any; + (controllerConstructor: IController, locals?: any, later?: any, ident?: any): any; (controllerName: string, locals?: any): any; } @@ -133,7 +160,8 @@ function noNg() { } var angular: { - bootstrap: (e: Element, modules: string[], config: IAngularBootstrapConfig) => void, + bootstrap: (e: Element, modules: (string | IInjectable)[], config: IAngularBootstrapConfig) => + void, module: (prefix: string, dependencies?: string[]) => IModule, element: (e: Element) => IAugmentedJQuery, version: {major: number}, resumeBootstrap?: () => void, diff --git a/modules/@angular/upgrade/src/aot.ts b/modules/@angular/upgrade/src/aot.ts new file mode 100644 index 0000000000..b50bda36cb --- /dev/null +++ b/modules/@angular/upgrade/src/aot.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export {downgradeComponent} from './aot/downgrade_component'; +export {downgradeInjectable} from './aot/downgrade_injectable'; +export {UpgradeComponent} from './aot/upgrade_component'; +export {UpgradeModule} from './aot/upgrade_module'; diff --git a/modules/@angular/upgrade/src/aot/angular1_providers.ts b/modules/@angular/upgrade/src/aot/angular1_providers.ts new file mode 100644 index 0000000000..0eb8714c33 --- /dev/null +++ b/modules/@angular/upgrade/src/aot/angular1_providers.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as angular from '../angular_js'; + +// We have to do a little dance to get the ng1 injector into the module injector. +// We store the ng1 injector so that the provider in the module injector can access it +// Then we "get" the ng1 injector from the module injector, which triggers the provider to read +// the stored injector and release the reference to it. +let tempInjectorRef: angular.IInjectorService; +export function setTempInjectorRef(injector: angular.IInjectorService) { + tempInjectorRef = injector; +} +export function injectorFactory() { + const injector: angular.IInjectorService = tempInjectorRef; + tempInjectorRef = null; // clear the value to prevent memory leaks + return injector; +} + +export function rootScopeFactory(i: angular.IInjectorService) { + return i.get('$rootScope'); +} + +export function compileFactory(i: angular.IInjectorService) { + return i.get('$compile'); +} + +export function parseFactory(i: angular.IInjectorService) { + return i.get('$parse'); +} + +export const angular1Providers = [ + // We must use exported named functions for the ng2 factories to keep the compiler happy: + // > Metadata collected contains an error that will be reported at runtime: + // > Function calls are not supported. + // > Consider replacing the function or lambda with a reference to an exported function + {provide: '$injector', useFactory: injectorFactory}, + {provide: '$rootScope', useFactory: rootScopeFactory, deps: ['$injector']}, + {provide: '$compile', useFactory: compileFactory, deps: ['$injector']}, + {provide: '$parse', useFactory: parseFactory, deps: ['$injector']} +]; diff --git a/modules/@angular/upgrade/src/aot/component_info.ts b/modules/@angular/upgrade/src/aot/component_info.ts new file mode 100644 index 0000000000..16a8a2df13 --- /dev/null +++ b/modules/@angular/upgrade/src/aot/component_info.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Type} from '@angular/core'; + +export interface ComponentInfo { + component: Type; + inputs?: string[]; + outputs?: string[]; +} + +export class PropertyBinding { + prop: string; + attr: string; + bracketAttr: string; + bracketParenAttr: string; + parenAttr: string; + onAttr: string; + bindAttr: string; + bindonAttr: string; + + constructor(public binding: string) { this.parseBinding(); } + + private parseBinding() { + const parts = this.binding.split(':'); + this.prop = parts[0].trim(); + this.attr = (parts[1] || this.prop).trim(); + this.bracketAttr = `[${this.attr}]`; + this.parenAttr = `(${this.attr})`; + this.bracketParenAttr = `[(${this.attr})]`; + const capitalAttr = this.attr.charAt(0).toUpperCase() + this.attr.substr(1); + this.onAttr = `on${capitalAttr}`; + this.bindAttr = `bind${capitalAttr}`; + this.bindonAttr = `bindon${capitalAttr}`; + } +} \ No newline at end of file diff --git a/modules/@angular/upgrade/src/aot/constants.ts b/modules/@angular/upgrade/src/aot/constants.ts new file mode 100644 index 0000000000..00d9702476 --- /dev/null +++ b/modules/@angular/upgrade/src/aot/constants.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export const UPGRADE_MODULE_NAME = '$$UpgradeModule'; +export const INJECTOR_KEY = '$$angularInjector'; + +export const $INJECTOR = '$injector'; +export const $PARSE = '$parse'; +export const $SCOPE = '$scope'; + +export const $COMPILE = '$compile'; +export const $TEMPLATE_CACHE = '$templateCache'; +export const $HTTP_BACKEND = '$httpBackend'; +export const $CONTROLLER = '$controller'; \ No newline at end of file diff --git a/modules/@angular/upgrade/src/aot/downgrade_component.ts b/modules/@angular/upgrade/src/aot/downgrade_component.ts new file mode 100644 index 0000000000..b79684ca26 --- /dev/null +++ b/modules/@angular/upgrade/src/aot/downgrade_component.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ComponentFactory, ComponentFactoryResolver, Injector} from '@angular/core'; + +import * as angular from '../angular_js'; + +import {ComponentInfo} from './component_info'; +import {$INJECTOR, $PARSE, INJECTOR_KEY} from './constants'; +import {DowngradeComponentAdapter} from './downgrade_component_adapter'; + +let downgradeCount = 0; + +/** + * @experimental + */ +export function downgradeComponent(info: ComponentInfo): angular.IInjectable { + const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`; + let idCount = 0; + + const directiveFactory: + angular.IAnnotatedFunction = function( + $injector: angular.IInjectorService, + $parse: angular.IParseService): angular.IDirective { + + return { + restrict: 'E', + require: '?^' + INJECTOR_KEY, + link: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes, + parentInjector: Injector, transclude: angular.ITranscludeFunction) => { + + if (parentInjector === null) { + parentInjector = $injector.get(INJECTOR_KEY); + } + + const componentFactoryResolver: ComponentFactoryResolver = + parentInjector.get(ComponentFactoryResolver); + const componentFactory: ComponentFactory = + componentFactoryResolver.resolveComponentFactory(info.component); + + if (!componentFactory) { + throw new Error('Expecting ComponentFactory for: ' + info.component); + } + + const facade = new DowngradeComponentAdapter( + idPrefix + (idCount++), info, element, attrs, scope, parentInjector, $parse, + componentFactory); + facade.setupInputs(); + facade.createComponent(); + facade.projectContent(); + facade.setupOutputs(); + facade.registerCleanup(); + } + }; + }; + + directiveFactory.$inject = [$INJECTOR, $PARSE]; + return directiveFactory; +} \ No newline at end of file diff --git a/modules/@angular/upgrade/src/aot/downgrade_component_adapter.ts b/modules/@angular/upgrade/src/aot/downgrade_component_adapter.ts new file mode 100644 index 0000000000..b9b7bd3fdc --- /dev/null +++ b/modules/@angular/upgrade/src/aot/downgrade_component_adapter.ts @@ -0,0 +1,180 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges, Type} from '@angular/core'; + +import * as angular from '../angular_js'; + +import {ComponentInfo, PropertyBinding} from './component_info'; +import {$SCOPE} from './constants'; + +const INITIAL_VALUE = { + __UNINITIALIZED__: true +}; + +export class DowngradeComponentAdapter { + component: any = null; + inputs: Attr; + inputChangeCount: number = 0; + inputChanges: SimpleChanges = null; + componentRef: ComponentRef = null; + changeDetector: ChangeDetectorRef = null; + componentScope: angular.IScope; + childNodes: Node[]; + contentInsertionPoint: 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 componentFactory: ComponentFactory) { + (this.element[0]).id = id; + this.componentScope = scope.$new(); + this.childNodes = element.contents(); + } + + createComponent() { + var childInjector = ReflectiveInjector.resolveAndCreate( + [{provide: $SCOPE, useValue: this.componentScope}], this.parentInjector); + this.contentInsertionPoint = document.createComment('ng1 insertion point'); + + this.componentRef = this.componentFactory.create( + childInjector, [[this.contentInsertionPoint]], this.element[0]); + this.changeDetector = this.componentRef.changeDetectorRef; + this.component = this.componentRef.instance; + } + + setupInputs(): void { + var attrs = this.attrs; + var inputs = this.info.inputs || []; + for (var i = 0; i < inputs.length; i++) { + var input = new PropertyBinding(inputs[i]); + var expr: any /** TODO #9100 */ = null; + + if (attrs.hasOwnProperty(input.attr)) { + var observeFn = ((prop: any /** TODO #9100 */) => { + var prevValue = INITIAL_VALUE; + return (value: any /** TODO #9100 */) => { + 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 as any /** TODO #9100 */)[input.bindAttr]; + } else if (attrs.hasOwnProperty(input.bracketAttr)) { + expr = (attrs as any /** TODO #9100 */)[input.bracketAttr]; + } else if (attrs.hasOwnProperty(input.bindonAttr)) { + expr = (attrs as any /** TODO #9100 */)[input.bindonAttr]; + } else if (attrs.hasOwnProperty(input.bracketParenAttr)) { + expr = (attrs as any /** TODO #9100 */)[input.bracketParenAttr]; + } + if (expr != null) { + var watchFn = + ((prop: any /** TODO #9100 */) => + (value: any /** TODO #9100 */, prevValue: any /** TODO #9100 */) => { + 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.component.prototype; + if (prototype && (prototype).ngOnChanges) { + // Detect: OnChanges interface + this.inputChanges = {}; + this.componentScope.$watch(() => this.inputChangeCount, () => { + var inputChanges = this.inputChanges; + this.inputChanges = {}; + (this.component).ngOnChanges(inputChanges); + }); + } + this.componentScope.$watch(() => this.changeDetector && this.changeDetector.detectChanges()); + } + + projectContent() { + var childNodes = this.childNodes; + var parent = this.contentInsertionPoint.parentNode; + if (parent) { + for (var i = 0, ii = childNodes.length; i < ii; i++) { + parent.insertBefore(childNodes[i], this.contentInsertionPoint); + } + } + } + + setupOutputs() { + var attrs = this.attrs; + var outputs = this.info.outputs || []; + for (var j = 0; j < outputs.length; j++) { + var output = new PropertyBinding(outputs[j]); + var expr: any /** TODO #9100 */ = 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 as any /** TODO #9100 */)[output.onAttr]; + } else if (attrs.hasOwnProperty(output.parenAttr)) { + expr = (attrs as any /** TODO #9100 */)[output.parenAttr]; + } else if (attrs.hasOwnProperty(bindonAttr)) { + expr = (attrs as any /** TODO #9100 */)[bindonAttr]; + assignExpr = true; + } else if (attrs.hasOwnProperty(bracketParenAttr)) { + expr = (attrs as any /** TODO #9100 */)[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] as EventEmitter; + if (emitter) { + emitter.subscribe({ + next: assignExpr ? + ((setter: any) => (v: any /** TODO #9100 */) => setter(this.scope, v))(setter) : + ((getter: any) => (v: any /** TODO #9100 */) => + getter(this.scope, {$event: v}))(getter) + }); + } else { + throw new Error( + `Missing emitter '${output.prop}' on component '${this.info.component}'!`); + } + } + } + } + + registerCleanup() { + this.element.bind('$destroy', () => { + this.componentScope.$destroy(); + this.componentRef.destroy(); + }); + } +} + +class Ng1Change implements SimpleChange { + constructor(public previousValue: any, public currentValue: any) {} + + isFirstChange(): boolean { return this.previousValue === this.currentValue; } +} diff --git a/modules/@angular/upgrade/src/aot/downgrade_injectable.ts b/modules/@angular/upgrade/src/aot/downgrade_injectable.ts new file mode 100644 index 0000000000..67a70dc952 --- /dev/null +++ b/modules/@angular/upgrade/src/aot/downgrade_injectable.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Injector} from '@angular/core'; +import {INJECTOR_KEY} from './constants'; + +/** + * Create an Angular 1 factory that will return an Angular 2 injectable thing + * (e.g. service, pipe, component, etc) + * + * Usage: + * + * ``` + * angular1Module.factory('someService', downgradeInjectable(SomeService)) + * ``` + * + * @experimental + */ +export function downgradeInjectable(token: any) { + return [INJECTOR_KEY, (i: Injector) => i.get(token)]; +} \ No newline at end of file diff --git a/modules/@angular/upgrade/src/aot/upgrade_component.ts b/modules/@angular/upgrade/src/aot/upgrade_component.ts new file mode 100644 index 0000000000..d7bbac5d40 --- /dev/null +++ b/modules/@angular/upgrade/src/aot/upgrade_component.ts @@ -0,0 +1,301 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnInit, SimpleChanges} from '@angular/core'; + +import * as angular from '../angular_js'; +import {looseIdentical} from '../facade/lang'; +import {controllerKey} from '../util'; + +import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $INJECTOR, $SCOPE, $TEMPLATE_CACHE} from './constants'; + +const NOT_SUPPORTED: any = 'NOT_SUPPORTED'; +const INITIAL_VALUE = { + __UNINITIALIZED__: true +}; + +class Bindings { + twoWayBoundProperties: string[] = []; + twoWayBoundLastValues: any[] = []; + + expressionBoundProperties: string[] = []; + + propertyToOutputMap: {[propName: string]: string} = {}; +} + +interface IBindingDestination { + [key: string]: any; + $onChanges?: (changes: SimpleChanges) => void; +} + +interface IControllerInstance extends IBindingDestination { + $onInit?: () => void; +} + +/** + * @experimental + */ +export class UpgradeComponent implements OnInit, OnChanges, DoCheck { + private $injector: angular.IInjectorService; + private $compile: angular.ICompileService; + private $templateCache: angular.ITemplateCacheService; + private $httpBackend: angular.IHttpBackendService; + private $controller: angular.IControllerService; + + private element: Element; + private $element: angular.IAugmentedJQuery; + private $componentScope: angular.IScope; + + private directive: angular.IDirective; + private bindings: Bindings; + private linkFn: angular.ILinkFn; + + private controllerInstance: IControllerInstance = null; + private bindingDestination: IBindingDestination = null; + + constructor(private name: string, private elementRef: ElementRef, private injector: Injector) { + this.$injector = injector.get($INJECTOR); + this.$compile = this.$injector.get($COMPILE); + this.$templateCache = this.$injector.get($TEMPLATE_CACHE); + this.$httpBackend = this.$injector.get($HTTP_BACKEND); + this.$controller = this.$injector.get($CONTROLLER); + + this.element = elementRef.nativeElement; + this.$element = angular.element(this.element); + + this.directive = this.getDirective(name); + this.bindings = this.initializeBindings(this.directive); + this.linkFn = this.compileTemplate(this.directive); + + // We ask for the Angular 1 scope from the Angular 2 injector, since + // we will put the new component scope onto the new injector for each component + const $parentScope = injector.get($SCOPE); + // QUESTION 1: Should we create an isolated scope if the scope is only true? + // QUESTION 2: Should we make the scope accessible through `$element.scope()/isolateScope()`? + this.$componentScope = $parentScope.$new(!!this.directive.scope); + + const controllerType = this.directive.controller; + // QUESTION: shouldn't we be building the controller in any case? + if (this.directive.bindToController) { + if (controllerType) { + this.bindingDestination = this.controllerInstance = this.buildController( + controllerType, this.$componentScope, this.$element, this.directive.controllerAs); + } else { + throw new Error( + `Upgraded directive '${name}' specifies 'bindToController' but no controller.`); + } + } else { + this.bindingDestination = this.$componentScope; + } + + this.setupOutputs(); + } + + ngOnInit() { + // QUESTION: why not just use $compile instead of reproducing parts of it + if (!this.directive.bindToController && this.directive.controller) { + this.controllerInstance = this.buildController( + this.directive.controller, this.$componentScope, this.$element, + this.directive.controllerAs); + } + const attrs: angular.IAttributes = NOT_SUPPORTED; + const transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED; + const linkController = this.resolveRequired(this.$element, this.directive.require); + + const link = this.directive.link; + const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre; + const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link; + if (preLink) { + preLink(this.$componentScope, this.$element, attrs, linkController, transcludeFn); + } + + var childNodes: Node[] = []; + var childNode: Node; + while (childNode = this.element.firstChild) { + this.element.removeChild(childNode); + childNodes.push(childNode); + } + + const attachElement: angular.ICloneAttachFunction = + (clonedElements, scope) => { this.$element.append(clonedElements); }; + const attachChildNodes: angular.ILinkFn = (scope, cloneAttach) => cloneAttach(childNodes); + + this.linkFn(this.$componentScope, attachElement, {parentBoundTranscludeFn: attachChildNodes}); + + if (postLink) { + postLink(this.$componentScope, this.$element, attrs, linkController, transcludeFn); + } + + if (this.controllerInstance && this.controllerInstance.$onInit) { + this.controllerInstance.$onInit(); + } + } + + ngOnChanges(changes: SimpleChanges) { + // Forward input changes to `bindingDestination` + Object.keys(changes).forEach( + propName => { this.bindingDestination[propName] = changes[propName].currentValue; }); + + if (this.bindingDestination.$onChanges) { + this.bindingDestination.$onChanges(changes); + } + } + + ngDoCheck() { + const twoWayBoundProperties = this.bindings.twoWayBoundProperties; + const twoWayBoundLastValues = this.bindings.twoWayBoundLastValues; + const propertyToOutputMap = this.bindings.propertyToOutputMap; + + twoWayBoundProperties.forEach((propName, idx) => { + const newValue = this.bindingDestination[propName]; + const oldValue = twoWayBoundLastValues[idx]; + + if (!looseIdentical(newValue, oldValue)) { + const outputName = propertyToOutputMap[propName]; + const eventEmitter: EventEmitter = (this as any)[outputName]; + + eventEmitter.emit(newValue); + twoWayBoundLastValues[idx] = newValue; + } + }); + } + + private getDirective(name: string): angular.IDirective { + const directives: angular.IDirective[] = this.$injector.get(name + 'Directive'); + if (directives.length > 1) { + throw new Error('Only support single directive definition for: ' + this.name); + } + const directive = directives[0]; + if (directive.replace) this.notSupported('replace'); + if (directive.terminal) this.notSupported('terminal'); + if (directive.compile) this.notSupported('compile'); + const link = directive.link; + // QUESTION: why not support link.post? + if (typeof link == 'object') { + if ((link).post) this.notSupported('link.post'); + } + return directive; + } + + private initializeBindings(directive: angular.IDirective) { + const btcIsObject = typeof directive.bindToController === 'object'; + if (btcIsObject && Object.keys(directive.scope).length) { + throw new Error( + `Binding definitions on scope and controller at the same time is not supported.`); + } + + const context = (btcIsObject) ? directive.bindToController : directive.scope; + const bindings = new Bindings(); + + if (typeof context == 'object') { + Object.keys(context).forEach(propName => { + const definition = context[propName]; + const bindingType = definition.charAt(0); + + // QUESTION: What about `=*`? Ignore? Throw? Support? + + switch (bindingType) { + case '@': + case '<': + // We don't need to do anything special. They will be defined as inputs on the + // upgraded component facade and the change propagation will be handled by + // `ngOnChanges()`. + break; + case '=': + bindings.twoWayBoundProperties.push(propName); + bindings.twoWayBoundLastValues.push(INITIAL_VALUE); + bindings.propertyToOutputMap[propName] = propName + 'Change'; + break; + case '&': + bindings.expressionBoundProperties.push(propName); + bindings.propertyToOutputMap[propName] = propName; + break; + default: + var json = JSON.stringify(context); + throw new Error( + `Unexpected mapping '${bindingType}' in '${json}' in '${this.name}' directive.`); + } + }); + } + + return bindings; + } + + private compileTemplate(directive: angular.IDirective): angular.ILinkFn { + if (this.directive.template !== undefined) { + return this.compileHtml(getOrCall(this.directive.template)); + } else if (this.directive.templateUrl) { + var url = getOrCall(this.directive.templateUrl); + var html = this.$templateCache.get(url) as string; + if (html !== undefined) { + return this.compileHtml(html); + } else { + throw new Error('loading directive templates asynchronously is not supported'); + // return new Promise((resolve, reject) => { + // this.$httpBackend('GET', url, null, (status: number, response: string) => { + // if (status == 200) { + // resolve(this.compileHtml(this.$templateCache.put(url, response))); + // } else { + // reject(`GET component template from '${url}' returned '${status}: ${response}'`); + // } + // }); + // }); + } + } else { + throw new Error(`Directive '${this.name}' is not a component, it is missing template.`); + } + } + + private buildController( + controllerType: angular.IController, $scope: angular.IScope, + $element: angular.IAugmentedJQuery, controllerAs: string) { + var locals = {$scope, $element}; + var controller = this.$controller(controllerType, locals, null, controllerAs); + $element.data(controllerKey(this.directive.name), controller); + return controller; + } + + private resolveRequired( + $element: angular.IAugmentedJQuery, require: angular.DirectiveRequireProperty) { + // TODO + } + + private setupOutputs() { + // Set up the outputs for `=` bindings + this.bindings.twoWayBoundProperties.forEach(propName => { + const outputName = this.bindings.propertyToOutputMap[propName]; + (this as any)[outputName] = new EventEmitter(); + }); + + // Set up the outputs for `&` bindings + this.bindings.expressionBoundProperties.forEach(propName => { + const outputName = this.bindings.propertyToOutputMap[propName]; + const emitter = (this as any)[outputName] = new EventEmitter(); + + // QUESTION: Do we want the ng1 component to call the function with `` or with + // `{$event: }`. The former is closer to ng2, the latter to ng1. + this.bindingDestination[propName] = (value: any) => emitter.emit(value); + }); + } + + private notSupported(feature: string) { + throw new Error( + `Upgraded directive '${this.name}' contains unsupported feature: '${feature}'.`); + } + + private compileHtml(html: string): angular.ILinkFn { + const div = document.createElement('div'); + div.innerHTML = html; + return this.$compile(div.childNodes); + } +} + + +function getOrCall(property: Function | T): T { + return typeof(property) === 'function' ? property() : property; +} diff --git a/modules/@angular/upgrade/src/aot/upgrade_module.ts b/modules/@angular/upgrade/src/aot/upgrade_module.ts new file mode 100644 index 0000000000..20fd234537 --- /dev/null +++ b/modules/@angular/upgrade/src/aot/upgrade_module.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Injector, NgModule, NgZone} from '@angular/core'; + +import * as angular from '../angular_js'; +import {controllerKey} from '../util'; + +import {angular1Providers, setTempInjectorRef} from './angular1_providers'; +import {$INJECTOR, INJECTOR_KEY, UPGRADE_MODULE_NAME} from './constants'; + + +/** + * The Ng1Module contains providers for the Ng1Adapter and all the core Angular 1 services; + * and also holds the `bootstrapNg1()` method fo bootstrapping an upgraded Angular 1 app. + * @experimental + */ +@NgModule({providers: angular1Providers}) +export class UpgradeModule { + public $injector: angular.IInjectorService; + + constructor(public injector: Injector, public ngZone: NgZone) {} + + /** + * Bootstrap an Angular 1 application from this NgModule + * @param element the element on which to bootstrap the Angular 1 application + * @param [modules] the Angular 1 modules to bootstrap for this application + * @param [config] optional extra Angular 1 bootstrap configuration + */ + bootstrap(element: Element, modules: string[] = [], config?: angular.IAngularBootstrapConfig) { + // Create an ng1 module to bootstrap + const upgradeModule = + angular + .module(UPGRADE_MODULE_NAME, modules) + + .value(INJECTOR_KEY, this.injector) + + .run([ + $INJECTOR, + ($injector: angular.IInjectorService) => { + this.$injector = $injector; + + // Initialize the ng1 $injector provider + setTempInjectorRef($injector); + this.injector.get($INJECTOR); + + // Put the injector on the DOM, so that it can be "required" + angular.element(element).data(controllerKey(INJECTOR_KEY), this.injector); + + // Wire up the ng1 rootScope to run a digest cycle whenever the zone settles + var $rootScope = $injector.get('$rootScope'); + this.ngZone.onMicrotaskEmpty.subscribe( + () => this.ngZone.runOutsideAngular(() => $rootScope.$evalAsync())); + } + ]); + + // Bootstrap the angular 1 application inside our zone + this.ngZone.run(() => { angular.bootstrap(element, [upgradeModule.name], config); }); + } +} diff --git a/modules/@angular/upgrade/src/constants.ts b/modules/@angular/upgrade/src/constants.ts index a1ecb0c9b0..36cb81e838 100644 --- a/modules/@angular/upgrade/src/constants.ts +++ b/modules/@angular/upgrade/src/constants.ts @@ -11,6 +11,7 @@ export const NG2_INJECTOR = 'ng2.Injector'; export const NG2_COMPONENT_FACTORY_REF_MAP = 'ng2.ComponentFactoryRefMap'; export const NG2_ZONE = 'ng2.NgZone'; +export const NG1_PROVIDE = '$provide'; export const NG1_CONTROLLER = '$controller'; export const NG1_SCOPE = '$scope'; export const NG1_ROOT_SCOPE = '$rootScope'; diff --git a/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts b/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts index bc4e60e425..d02ea05bf9 100644 --- a/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts +++ b/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts @@ -49,7 +49,7 @@ export class DowngradeNg2ComponentAdapter { setupInputs(): void { var attrs = this.attrs; - var inputs = this.info.inputs; + var inputs = this.info.inputs || []; for (var i = 0; i < inputs.length; i++) { var input = inputs[i]; var expr: any /** TODO #9100 */ = null; @@ -115,7 +115,7 @@ export class DowngradeNg2ComponentAdapter { setupOutputs() { var attrs = this.attrs; - var outputs = this.info.outputs; + var outputs = this.info.outputs || []; for (var j = 0; j < outputs.length; j++) { var output = outputs[j]; var expr: any /** TODO #9100 */ = null; diff --git a/modules/@angular/upgrade/src/facade b/modules/@angular/upgrade/src/facade new file mode 120000 index 0000000000..e084c803c6 --- /dev/null +++ b/modules/@angular/upgrade/src/facade @@ -0,0 +1 @@ +../../facade/src \ No newline at end of file diff --git a/modules/@angular/upgrade/src/metadata.ts b/modules/@angular/upgrade/src/metadata.ts index 1510769a94..25d15429d6 100644 --- a/modules/@angular/upgrade/src/metadata.ts +++ b/modules/@angular/upgrade/src/metadata.ts @@ -27,8 +27,8 @@ export interface AttrProp { export interface ComponentInfo { type: Type; selector: string; - inputs: AttrProp[]; - outputs: AttrProp[]; + inputs?: AttrProp[]; + outputs?: AttrProp[]; } export function getComponentInfo(type: Type): ComponentInfo { diff --git a/modules/@angular/upgrade/src/upgrade_ng1_adapter.ts b/modules/@angular/upgrade/src/upgrade_ng1_adapter.ts index 7d1c41b595..b93296a352 100644 --- a/modules/@angular/upgrade/src/upgrade_ng1_adapter.ts +++ b/modules/@angular/upgrade/src/upgrade_ng1_adapter.ts @@ -248,7 +248,7 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { this.element.removeChild(childNode); childNodes.push(childNode); } - this.linkFn(this.componentScope, (clonedElement: Node[], scope: angular.IScope) => { + this.linkFn(this.componentScope, (clonedElement, scope) => { for (var i = 0, ii = clonedElement.length; i < ii; i++) { this.element.appendChild(clonedElement[i]); } @@ -302,7 +302,8 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { return controller; } - private resolveRequired($element: angular.IAugmentedJQuery, require: string|string[]): any { + private resolveRequired( + $element: angular.IAugmentedJQuery, require: angular.DirectiveRequireProperty): any { if (!require) { return undefined; } else if (typeof require == 'string') { diff --git a/modules/@angular/upgrade/test/aot/angular1_providers_spec.ts b/modules/@angular/upgrade/test/aot/angular1_providers_spec.ts new file mode 100644 index 0000000000..9dc1188d7e --- /dev/null +++ b/modules/@angular/upgrade/test/aot/angular1_providers_spec.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Ng1Token} from '@angular/upgrade/src/angular_js'; +import {compileFactory, injectorFactory, parseFactory, rootScopeFactory, setTempInjectorRef} from '@angular/upgrade/src/aot/angular1_providers'; + +export function main() { + describe('upgrade angular1_providers', () => { + describe('compileFactory', () => { + it('should retrieve and return `$compile`', () => { + const services: {[key: string]: any} = {$compile: 'foo'}; + const mockInjector = {get: (name: Ng1Token): any => services[name], has: () => true}; + + expect(compileFactory(mockInjector)).toBe('foo'); + }); + }); + + describe('injectorFactory', () => { + it('should return the injector value that was previously set', () => { + const mockInjector = {get: () => {}, has: () => false}; + setTempInjectorRef(mockInjector); + const injector = injectorFactory(); + expect(injector).toBe(mockInjector); + }); + + it('should unset the injector after the first call (to prevent memory leaks)', () => { + const mockInjector = {get: () => {}, has: () => false}; + setTempInjectorRef(mockInjector); + injectorFactory(); + const injector = injectorFactory(); + expect(injector).toBe(null); + }); + }); + + describe('parseFactory', () => { + it('should retrieve and return `$parse`', () => { + const services: {[key: string]: any} = {$parse: 'bar'}; + const mockInjector = {get: (name: Ng1Token): any => services[name], has: () => true}; + + expect(parseFactory(mockInjector)).toBe('bar'); + }); + }); + + describe('rootScopeFactory', () => { + it('should retrieve and return `$rootScope`', () => { + const services: {[key: string]: any} = {$rootScope: 'baz'}; + const mockInjector = {get: (name: Ng1Token): any => services[name], has: () => true}; + + expect(rootScopeFactory(mockInjector)).toBe('baz'); + }); + }); + }); +} \ No newline at end of file diff --git a/modules/@angular/upgrade/test/aot/component_info_spec.ts b/modules/@angular/upgrade/test/aot/component_info_spec.ts new file mode 100644 index 0000000000..fc71925525 --- /dev/null +++ b/modules/@angular/upgrade/test/aot/component_info_spec.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {PropertyBinding} from '@angular/upgrade/src/aot/component_info'; + +export function main() { + describe('PropertyBinding', () => { + it('should process a simple binding', () => { + const binding = new PropertyBinding('someBinding'); + expect(binding.binding).toEqual('someBinding'); + expect(binding.prop).toEqual('someBinding'); + expect(binding.attr).toEqual('someBinding'); + expect(binding.bracketAttr).toEqual('[someBinding]'); + expect(binding.bracketParenAttr).toEqual('[(someBinding)]'); + expect(binding.parenAttr).toEqual('(someBinding)'); + expect(binding.onAttr).toEqual('onSomeBinding'); + expect(binding.bindAttr).toEqual('bindSomeBinding'); + expect(binding.bindonAttr).toEqual('bindonSomeBinding'); + }); + + it('should process a two-part binding', () => { + const binding = new PropertyBinding('someProp:someAttr'); + expect(binding.binding).toEqual('someProp:someAttr'); + expect(binding.prop).toEqual('someProp'); + expect(binding.attr).toEqual('someAttr'); + expect(binding.bracketAttr).toEqual('[someAttr]'); + expect(binding.bracketParenAttr).toEqual('[(someAttr)]'); + expect(binding.parenAttr).toEqual('(someAttr)'); + expect(binding.onAttr).toEqual('onSomeAttr'); + expect(binding.bindAttr).toEqual('bindSomeAttr'); + expect(binding.bindonAttr).toEqual('bindonSomeAttr'); + }); + + it('should cope with whitespace', () => { + const binding = new PropertyBinding(' someProp : someAttr '); + expect(binding.binding).toEqual(' someProp : someAttr '); + expect(binding.prop).toEqual('someProp'); + expect(binding.attr).toEqual('someAttr'); + expect(binding.bracketAttr).toEqual('[someAttr]'); + expect(binding.bracketParenAttr).toEqual('[(someAttr)]'); + expect(binding.parenAttr).toEqual('(someAttr)'); + expect(binding.onAttr).toEqual('onSomeAttr'); + expect(binding.bindAttr).toEqual('bindSomeAttr'); + expect(binding.bindonAttr).toEqual('bindonSomeAttr'); + }); + }); +} diff --git a/modules/@angular/upgrade/test/aot/downgrade_injectable_spec.ts b/modules/@angular/upgrade/test/aot/downgrade_injectable_spec.ts new file mode 100644 index 0000000000..e91d5b8573 --- /dev/null +++ b/modules/@angular/upgrade/test/aot/downgrade_injectable_spec.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {INJECTOR_KEY} from '@angular/upgrade/src/aot/constants'; +import {downgradeInjectable} from '@angular/upgrade/src/aot/downgrade_injectable'; + +export function main() { + describe('downgradeInjectable', () => { + it('should return an Angular 1 annotated factory for the token', () => { + const factory = downgradeInjectable('someToken'); + expect(factory[0]).toEqual(INJECTOR_KEY); + expect(factory[1]).toEqual(jasmine.any(Function)); + const injector = {get: jasmine.createSpy('get').and.returnValue('service value')}; + const value = (factory as any)[1](injector); + expect(injector.get).toHaveBeenCalledWith('someToken'); + expect(value).toEqual('service value'); + }); + }); +} diff --git a/modules/@angular/upgrade/test/aot/integration/change_detection_spec.ts b/modules/@angular/upgrade/test/aot/integration/change_detection_spec.ts new file mode 100644 index 0000000000..876e20ecf6 --- /dev/null +++ b/modules/@angular/upgrade/test/aot/integration/change_detection_spec.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, Directive, ElementRef, Injector, NgModule, destroyPlatform} from '@angular/core'; +import {async} from '@angular/core/testing'; +import {BrowserModule} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade'; +import * as angular from '@angular/upgrade/src/angular_js'; + +import {bootstrap, html} from '../test_helpers'; + +export function main() { + describe('scope/component change-detection', () => { + beforeEach(() => destroyPlatform()); + afterEach(() => destroyPlatform()); + + it('should interleave scope and component expressions', async(() => { + const log: any[] /** TODO #9100 */ = []; + const l = (value: any /** TODO #9100 */) => { + log.push(value); + return value + ';'; + }; + + @Directive({selector: 'ng1a'}) + class Ng1aComponent extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1a', elementRef, injector); + } + } + + @Directive({selector: 'ng1b'}) + class Ng1bComponent extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1b', elementRef, injector); + } + } + + @Component({ + selector: 'ng2', + template: `{{l('2A')}}{{l('2B')}}{{l('2C')}}` + }) + class Ng2Component { + l: (value: any) => string; + constructor() { this.l = l; } + } + + @NgModule({ + declarations: [Ng1aComponent, Ng1bComponent, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const ng1Module = angular.module('ng1', []) + .directive('ng1a', () => ({template: '{{ l(\'ng1a\') }}'})) + .directive('ng1b', () => ({template: '{{ l(\'ng1b\') }}'})) + .directive('ng2', downgradeComponent({component: Ng2Component})) + .run(($rootScope: any /** TODO #9100 */) => { + $rootScope.l = l; + $rootScope.reset = () => log.length = 0; + }); + + const element = + html('
{{reset(); l(\'1A\');}}{{l(\'1B\')}}{{l(\'1C\')}}
'); + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + 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']); + }); + })); + }); +} \ No newline at end of file diff --git a/modules/@angular/upgrade/test/aot/integration/content_projection_spec.ts b/modules/@angular/upgrade/test/aot/integration/content_projection_spec.ts new file mode 100644 index 0000000000..4a37258be6 --- /dev/null +++ b/modules/@angular/upgrade/test/aot/integration/content_projection_spec.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, Directive, ElementRef, Injector, NgModule, destroyPlatform} from '@angular/core'; +import {async} from '@angular/core/testing'; +import {BrowserModule} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade'; +import * as angular from '@angular/upgrade/src/angular_js'; + +import {bootstrap, html} from '../test_helpers'; + +export function main() { + describe('content projection', () => { + + beforeEach(() => destroyPlatform()); + afterEach(() => destroyPlatform()); + + it('should instantiate ng2 in ng1 template and project content', async(() => { + + // the ng2 component that will be used in ng1 (downgraded) + @Component({selector: 'ng2', template: `{{ 'NG2' }}()`}) + class Ng2Component { + } + + // our upgrade module to host the component to downgrade + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng2Component], + entryComponents: [Ng2Component] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // the ng1 app module that will consume the downgraded component + const ng1Module = angular + .module('ng1', []) + // create an ng1 facade of the ng2 component + .directive('ng2', downgradeComponent({component: Ng2Component})); + + const element = + html('
{{ \'ng1[\' }}~{{ \'ng-content\' }}~{{ \']\' }}
'); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + expect(document.body.textContent).toEqual('ng1[NG2(~ng-content~)]'); + }); + })); + + it('should instantiate ng1 in ng2 template and project content', async(() => { + + @Component({ + selector: 'ng2', + template: `{{ 'ng2(' }}{{'transclude'}}{{ ')' }}`, + }) + class Ng2Component { + } + + + @Directive({selector: 'ng1'}) + class Ng1WrapperComponent extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + @NgModule({ + declarations: [Ng1WrapperComponent, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const ng1Module = angular.module('ng1', []) + .directive( + 'ng1', + () => { + return { + transclude: true, + template: '{{ "ng1" }}()' + }; + }) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + const element = html('
{{\'ng1(\'}}{{\')\'}}
'); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + expect(document.body.textContent).toEqual('ng1(ng2(ng1(transclude)))'); + }); + })); + }); +} diff --git a/modules/@angular/upgrade/test/aot/integration/downgrade_component_spec.ts b/modules/@angular/upgrade/test/aot/integration/downgrade_component_spec.ts new file mode 100644 index 0000000000..861c8ba418 --- /dev/null +++ b/modules/@angular/upgrade/test/aot/integration/downgrade_component_spec.ts @@ -0,0 +1,259 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, EventEmitter, NgModule, OnChanges, OnDestroy, SimpleChanges, destroyPlatform} from '@angular/core'; +import {async} from '@angular/core/testing'; +import {BrowserModule} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {UpgradeModule, downgradeComponent} from '@angular/upgrade'; +import * as angular from '@angular/upgrade/src/angular_js'; + +import {bootstrap, html, multiTrim} from '../test_helpers'; + +export function main() { + describe('downgrade ng2 component', () => { + + beforeEach(() => destroyPlatform()); + afterEach(() => destroyPlatform()); + + it('should bind properties, events', async(() => { + + const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { + $rootScope['dataA'] = 'A'; + $rootScope['dataB'] = 'B'; + $rootScope['modelA'] = 'initModelA'; + $rootScope['modelB'] = 'initModelB'; + $rootScope['eventA'] = '?'; + $rootScope['eventB'] = '?'; + }); + + @Component({ + selector: 'ng2', + inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], + outputs: [ + 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' + ], + template: 'ignore: {{ignore}}; ' + + 'literal: {{literal}}; interpolate: {{interpolate}}; ' + + 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + + 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' + }) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + ignore = '-'; + literal = '?'; + interpolate = '?'; + oneWayA = '?'; + oneWayB = '?'; + twoWayA = '?'; + twoWayB = '?'; + eventA = new EventEmitter(); + eventB = new EventEmitter(); + twoWayAEmitter = new EventEmitter(); + twoWayBEmitter = new EventEmitter(); + + ngOnChanges(changes: SimpleChanges) { + const assert = (prop: string, value: any) => { + const propVal = (this as any)[prop]; + if (propVal != value) { + throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`); + } + }; + + const assertChange = (prop: string, value: any) => { + assert(prop, value); + if (!changes[prop]) { + throw new Error(`Changes record for '${prop}' not found.`); + } + const actualValue = changes[prop].currentValue; + if (actualValue != value) { + throw new Error( + `Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`); + } + }; + + switch (this.ngOnChangesCount++) { + case 0: + assert('ignore', '-'); + assertChange('literal', 'Text'); + assertChange('interpolate', 'Hello world'); + assertChange('oneWayA', 'A'); + assertChange('oneWayB', 'B'); + assertChange('twoWayA', 'initModelA'); + assertChange('twoWayB', 'initModelB'); + + this.twoWayAEmitter.emit('newA'); + this.twoWayBEmitter.emit('newB'); + this.eventA.emit('aFired'); + this.eventB.emit('bFired'); + break; + case 1: + assertChange('twoWayA', 'newA'); + break; + case 2: + assertChange('twoWayB', 'newB'); + break; + default: + throw new Error('Called too many times! ' + JSON.stringify(changes)); + } + }; + } + + ng1Module.directive( + 'ng2', downgradeComponent({ + component: Ng2Component, + inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], + outputs: [ + 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', + 'twoWayBEmitter: twoWayBChange' + ] + })); + + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const element = html(` +
+ + | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}}; +
`); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + expect(multiTrim(document.body.textContent)) + .toEqual( + 'ignore: -; ' + + 'literal: Text; interpolate: Hello world; ' + + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); + }); + })); + + it('should properly run cleanup when ng1 directive is destroyed', async(() => { + + let destroyed = false; + @Component({selector: 'ng2', template: 'test'}) + class Ng2Component implements OnDestroy { + ngOnDestroy() { destroyed = true; } + } + + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const ng1Module = + angular.module('ng1', []) + .directive( + 'ng1', + () => { return {template: '
'}; }) + .directive('ng2', downgradeComponent({component: Ng2Component})); + const element = html(''); + platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => { + const adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + expect(element.textContent).toContain('test'); + expect(destroyed).toBe(false); + + const $rootScope = adapter.$injector.get('$rootScope'); + $rootScope.$apply('destroyIt = true'); + + expect(element.textContent).not.toContain('test'); + expect(destroyed).toBe(true); + }); + })); + + it('should work when compiled outside the dom (by fallback to the root ng2.injector)', + async(() => { + + @Component({selector: 'ng2', template: 'test'}) + class Ng2Component { + } + + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const ng1Module = + angular.module('ng1', []) + .directive( + 'ng1', + [ + '$compile', + ($compile: angular.ICompileService) => { + return { + link: function( + $scope: angular.IScope, $element: angular.IAugmentedJQuery, + $attrs: angular.IAttributes) { + // here we compile some HTML that contains a downgraded component + // since it is not currently in the DOM it is not able to "require" + // an ng2 injector so it should use the `moduleInjector` instead. + const compiled = $compile(''); + const template = compiled($scope); + $element.append(template); + } + }; + } + ]) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + const element = html(''); + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + // the fact that the body contains the correct text means that the + // downgraded component was able to access the moduleInjector + // (since there is no other injector in this system) + expect(multiTrim(document.body.textContent)).toEqual('test'); + }); + })); + + it('should allow attribute selectors for components in ng2', async(() => { + @Component({selector: '[itWorks]', template: 'It works'}) + class WorksComponent { + } + + @Component({selector: 'root-component', template: '!'}) + class RootComponent { + } + + @NgModule({ + declarations: [RootComponent, WorksComponent], + entryComponents: [RootComponent], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const ng1Module = angular.module('ng1', []).directive( + 'rootComponent', downgradeComponent({component: RootComponent})); + + const element = html(''); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + expect(multiTrim(document.body.textContent)).toBe('It works!'); + }); + })); + }); +} \ No newline at end of file diff --git a/modules/@angular/upgrade/test/aot/integration/examples_spec.ts b/modules/@angular/upgrade/test/aot/integration/examples_spec.ts new file mode 100644 index 0000000000..6c847c4a31 --- /dev/null +++ b/modules/@angular/upgrade/test/aot/integration/examples_spec.ts @@ -0,0 +1,89 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, Directive, ElementRef, Injector, Input, NgModule, destroyPlatform} from '@angular/core'; +import {async} from '@angular/core/testing'; +import {BrowserModule} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade'; +import * as angular from '@angular/upgrade/src/angular_js'; + +import {bootstrap, html, multiTrim} from '../test_helpers'; + +export function main() { + describe('examples', () => { + + beforeEach(() => destroyPlatform()); + afterEach(() => destroyPlatform()); + + it('should have angular 1 loaded', () => expect(angular.version.major).toBe(1)); + + it('should verify UpgradeAdapter example', async(() => { + + // This is wrapping (upgrading) an Angular 1 component to be used in an Angular 2 + // component + @Directive({selector: 'ng1'}) + class Ng1Component extends UpgradeComponent { + @Input() title: string; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // This is an Angular 2 component that will be downgraded + @Component({ + selector: 'ng2', + template: 'ng2[transclude]()' + }) + class Ng2Component { + @Input('name') nameProp: string; + } + + // This module represents the Angular 2 pieces of the application + @NgModule({ + declarations: [Ng1Component, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() { /* this is a placeholder to stop the boostrapper from complaining */ + } + } + + // This module represents the Angular 1 pieces of the application + const ng1Module = + angular + .module('myExample', []) + // This is an Angular 1 component that will be upgraded + .directive( + 'ng1', + () => { + return { + scope: {title: '='}, + transclude: true, + template: 'ng1[Hello {{title}}!]()' + }; + }) + // This is wrapping (downgrading) an Angular 2 component to be used in Angular 1 + .directive( + 'ng2', + downgradeComponent({component: Ng2Component, inputs: ['nameProp: name']})); + + // This is the (Angular 1) application bootstrap element + // Notice that it is actually a downgraded Angular 2 component + const element = html('project'); + + // Let's use a helper function to make this simpler + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { + expect(multiTrim(element.textContent)) + .toBe('ng2[ng1[Hello World!](transclude)](project)'); + }); + })); + }); +} \ No newline at end of file diff --git a/modules/@angular/upgrade/test/aot/integration/injection_spec.ts b/modules/@angular/upgrade/test/aot/integration/injection_spec.ts new file mode 100644 index 0000000000..7a818c4fde --- /dev/null +++ b/modules/@angular/upgrade/test/aot/integration/injection_spec.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NgModule, OpaqueToken, destroyPlatform} from '@angular/core'; +import {async} from '@angular/core/testing'; +import {BrowserModule} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {UpgradeModule, downgradeInjectable} from '@angular/upgrade'; +import * as angular from '@angular/upgrade/src/angular_js'; + +import {bootstrap, html} from '../test_helpers'; + +export function main() { + describe('injection', () => { + + beforeEach(() => destroyPlatform()); + afterEach(() => destroyPlatform()); + + it('should downgrade ng2 service to ng1', async(() => { + // Tokens used in ng2 to identify services + const Ng2Service = new OpaqueToken('ng2-service'); + + // Sample ng1 NgModule for tests + @NgModule({ + imports: [BrowserModule, UpgradeModule], + providers: [ + {provide: Ng2Service, useValue: 'ng2 service value'}, + ] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // create the ng1 module that will import an ng2 service + const ng1Module = + angular.module('ng1Module', []).factory('ng2Service', downgradeInjectable(Ng2Service)); + + bootstrap(platformBrowserDynamic(), Ng2Module, html('
'), ng1Module) + .then((upgrade) => { + const ng1Injector = upgrade.$injector; + expect(ng1Injector.get('ng2Service')).toBe('ng2 service value'); + }); + })); + + it('should upgrade ng1 service to ng2', async(() => { + // Tokens used in ng2 to identify services + const Ng1Service = new OpaqueToken('ng1-service'); + + // Sample ng1 NgModule for tests + @NgModule({ + imports: [BrowserModule, UpgradeModule], + providers: [ + // the following line is the "upgrade" of an Angular 1 service + { + provide: Ng1Service, + useFactory: (i: angular.IInjectorService) => i.get('ng1Service'), + deps: ['$injector'] + } + ] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // create the ng1 module that will import an ng2 service + const ng1Module = angular.module('ng1Module', []).value('ng1Service', 'ng1 service value'); + + bootstrap(platformBrowserDynamic(), Ng2Module, html('
'), ng1Module) + .then((upgrade) => { + var ng2Injector = upgrade.injector; + expect(ng2Injector.get(Ng1Service)).toBe('ng1 service value'); + }); + })); + }); +} \ No newline at end of file diff --git a/modules/@angular/upgrade/test/aot/integration/testability_spec.ts b/modules/@angular/upgrade/test/aot/integration/testability_spec.ts new file mode 100644 index 0000000000..991b84882e --- /dev/null +++ b/modules/@angular/upgrade/test/aot/integration/testability_spec.ts @@ -0,0 +1,71 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NgModule, Testability, destroyPlatform} from '@angular/core'; +import {fakeAsync, tick} from '@angular/core/testing'; +import {BrowserModule} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {UpgradeModule} from '@angular/upgrade'; +import * as angular from '@angular/upgrade/src/angular_js'; + +import {bootstrap, html} from '../test_helpers'; + +export function main() { + describe('testability', () => { + + beforeEach(() => destroyPlatform()); + afterEach(() => destroyPlatform()); + + @NgModule({imports: [BrowserModule, UpgradeModule]}) + class Ng2Module { + ngDoBootstrap() {} + } + + it('should handle deferred bootstrap', fakeAsync(() => { + let applicationRunning = false; + const ng1Module = angular.module('ng1', []).run(() => { applicationRunning = true; }); + + const element = html('
'); + window.name = 'NG_DEFER_BOOTSTRAP!' + window.name; + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module); + + setTimeout(() => { (window).angular.resumeBootstrap(); }, 100); + + expect(applicationRunning).toEqual(false); + tick(100); + expect(applicationRunning).toEqual(true); + })); + + it('should wait for ng2 testability', fakeAsync(() => { + const ng1Module = angular.module('ng1', []); + const element = html('
'); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + + const ng2Testability: Testability = upgrade.injector.get(Testability); + ng2Testability.increasePendingRequestCount(); + let ng2Stable = false; + let ng1Stable = false; + + angular.getTestability(element).whenStable(() => { ng1Stable = true; }); + + setTimeout(() => { + ng2Stable = true; + ng2Testability.decreasePendingRequestCount(); + }, 100); + + expect(ng1Stable).toEqual(false); + expect(ng2Stable).toEqual(false); + tick(100); + expect(ng1Stable).toEqual(true); + expect(ng2Stable).toEqual(true); + }); + })); + }); +} \ No newline at end of file diff --git a/modules/@angular/upgrade/test/aot/integration/upgrade_component_spec.ts b/modules/@angular/upgrade/test/aot/integration/upgrade_component_spec.ts new file mode 100644 index 0000000000..c489529859 --- /dev/null +++ b/modules/@angular/upgrade/test/aot/integration/upgrade_component_spec.ts @@ -0,0 +1,1564 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, Directive, ElementRef, EventEmitter, Injector, Input, NO_ERRORS_SCHEMA, NgModule, Output, destroyPlatform} from '@angular/core'; +import {async, fakeAsync, tick} from '@angular/core/testing'; +import {BrowserModule} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade'; +import * as angular from '@angular/upgrade/src/angular_js'; + +import {bootstrap, html, multiTrim} from '../test_helpers'; + +export function main() { + describe('upgrade ng1 component', () => { + + beforeEach(() => destroyPlatform()); + afterEach(() => destroyPlatform()); + + describe('template/templateUrl', () => { + it('should support `template` (string)', async(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = {template: 'Hello, Angular!'}; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + expect(multiTrim(element.textContent)).toBe('Hello, Angular!'); + }); + })); + + it('should support `template` (function)', async(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = {template: () => 'Hello, Angular!'}; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(element.textContent)).toBe('Hello, Angular!'); + }); + })); + + it('should support not pass any arguments to `template` function', async(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: ($attrs: angular.IAttributes, $element: angular.IAugmentedJQuery) => { + expect($attrs).toBeUndefined(); + expect($element).toBeUndefined(); + + return 'Hello, Angular!'; + } + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(element.textContent)).toBe('Hello, Angular!'); + }); + })); + + it('should support `templateUrl` (string) fetched from `$templateCache`', async(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = {templateUrl: 'ng1.component.html'}; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = + angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})) + .run( + ($templateCache: angular.ITemplateCacheService) => + $templateCache.put('ng1.component.html', 'Hello, Angular!')); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(element.textContent)).toBe('Hello, Angular!'); + }); + })); + + it('should support `templateUrl` (function) fetched from `$templateCache`', async(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = {templateUrl: () => 'ng1.component.html'}; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = + angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})) + .run( + ($templateCache: angular.ITemplateCacheService) => + $templateCache.put('ng1.component.html', 'Hello, Angular!')); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(element.textContent)).toBe('Hello, Angular!'); + }); + })); + + it('should support not pass any arguments to `templateUrl` function', async(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = { + templateUrl: ($attrs: angular.IAttributes, $element: angular.IAugmentedJQuery) => { + expect($attrs).toBeUndefined(); + expect($element).toBeUndefined(); + + return 'ng1.component.html'; + } + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = + angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})) + .run( + ($templateCache: angular.ITemplateCacheService) => + $templateCache.put('ng1.component.html', 'Hello, Angular!')); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(element.textContent)).toBe('Hello, Angular!'); + }); + })); + + // NOT SUPPORTED YET + xit('should support `templateUrl` (string) fetched from the server', fakeAsync(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = {templateUrl: 'ng1.component.html'}; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = + angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})) + .value( + '$httpBackend', + (method: string, url: string, post?: any, callback?: Function) => + setTimeout( + () => callback(200, `${method}:${url}`.toLowerCase()), 1000)); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + tick(500); + expect(multiTrim(element.textContent)).toBe(''); + + tick(500); + expect(multiTrim(element.textContent)).toBe('get:ng1.component.html'); + }); + })); + + // NOT SUPPORTED YET + xit('should support `templateUrl` (function) fetched from the server', fakeAsync(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = {templateUrl: () => 'ng1.component.html'}; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = + angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})) + .value( + '$httpBackend', + (method: string, url: string, post?: any, callback?: Function) => + setTimeout( + () => callback(200, `${method}:${url}`.toLowerCase()), 1000)); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + tick(500); + expect(multiTrim(element.textContent)).toBe(''); + + tick(500); + expect(multiTrim(element.textContent)).toBe('get:ng1.component.html'); + }); + })); + + it('should support empty templates', async(() => { + // Define `ng1Component`s + const ng1ComponentA: angular.IComponent = {template: ''}; + const ng1ComponentB: angular.IComponent = {template: () => ''}; + const ng1ComponentC: angular.IComponent = {templateUrl: 'ng1.component.html'}; + const ng1ComponentD: angular.IComponent = {templateUrl: () => 'ng1.component.html'}; + + // Define `Ng1ComponentFacade`s + @Directive({selector: 'ng1A'}) + class Ng1ComponentAFacade extends UpgradeComponent { + constructor(e: ElementRef, i: Injector) { super('ng1A', e, i); } + } + @Directive({selector: 'ng1B'}) + class Ng1ComponentBFacade extends UpgradeComponent { + constructor(e: ElementRef, i: Injector) { super('ng1B', e, i); } + } + @Directive({selector: 'ng1C'}) + class Ng1ComponentCFacade extends UpgradeComponent { + constructor(e: ElementRef, i: Injector) { super('ng1C', e, i); } + } + @Directive({selector: 'ng1D'}) + class Ng1ComponentDFacade extends UpgradeComponent { + constructor(e: ElementRef, i: Injector) { super('ng1D', e, i); } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + Ignore this + Ignore this + Ignore this + Ignore this + ` + }) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1A', ng1ComponentA) + .component('ng1B', ng1ComponentB) + .component('ng1C', ng1ComponentC) + .component('ng1D', ng1ComponentD) + .directive('ng2', downgradeComponent({component: Ng2Component})) + .run( + ($templateCache: angular.ITemplateCacheService) => + $templateCache.put('ng1.component.html', '')); + + // Define `Ng2Module` + @NgModule({ + declarations: [ + Ng1ComponentAFacade, Ng1ComponentBFacade, Ng1ComponentCFacade, Ng1ComponentDFacade, + Ng2Component + ], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(element.textContent)).toBe(''); + }); + })); + }); + + describe('bindings', () => { + it('should support `@` bindings', fakeAsync(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: 'Inside: {{ $ctrl.inputA }}, {{ $ctrl.inputB }}', + bindings: {inputA: '@inputAttrA', inputB: '@'} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + @Input('inputAttrA') inputA: string; + @Input() inputB: string; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + + | Outside: {{ dataA }}, {{ dataB }} + ` + }) + class Ng2Component { + dataA = 'foo'; + dataB = 'bar'; + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + var ng1 = element.querySelector('ng1'); + var ng1Controller = angular.element(ng1).controller('ng1'); + + expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); + + ng1Controller.inputA = 'baz'; + ng1Controller.inputB = 'qux'; + tick(); + + expect(multiTrim(element.textContent)).toBe('Inside: baz, qux | Outside: foo, bar'); + + // TODO: Verify that changes in `` propagate to ``. + }); + })); + + it('should support `<` bindings', fakeAsync(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: 'Inside: {{ $ctrl.inputA.value }}, {{ $ctrl.inputB.value }}', + bindings: {inputA: ' + | Outside: {{ dataA.value }}, {{ dataB.value }} + ` + }) + class Ng2Component { + dataA = {value: 'foo'}; + dataB = {value: 'bar'}; + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + var ng1 = element.querySelector('ng1'); + var ng1Controller = angular.element(ng1).controller('ng1'); + + expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); + + ng1Controller.inputA = {value: 'baz'}; + ng1Controller.inputB = {value: 'qux'}; + tick(); + + expect(multiTrim(element.textContent)).toBe('Inside: baz, qux | Outside: foo, bar'); + + // TODO: Verify that changes in `` propagate to ``. + }); + })); + + it('should support `=` bindings', fakeAsync(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: 'Inside: {{ $ctrl.inputA.value }}, {{ $ctrl.inputB.value }}', + bindings: {inputA: '=inputAttrA', inputB: '='} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + @Input('inputAttrA') inputA: string; + @Output('inputAttrAChange') inputAChange: EventEmitter; + @Input() inputB: string; + @Output() inputBChange: EventEmitter; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + + | Outside: {{ dataA.value }}, {{ dataB.value }} + ` + }) + class Ng2Component { + dataA = {value: 'foo'}; + dataB = {value: 'bar'}; + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + var ng1 = element.querySelector('ng1'); + var ng1Controller = angular.element(ng1).controller('ng1'); + + expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); + + ng1Controller.inputA = {value: 'baz'}; + ng1Controller.inputB = {value: 'qux'}; + tick(); + + expect(multiTrim(element.textContent)).toBe('Inside: baz, qux | Outside: baz, qux'); + + // TODO: Verify that changes in `` propagate to ``. + }); + })); + + it('should support `&` bindings', fakeAsync(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: 'Inside: -', + bindings: {outputA: '&outputAttrA', outputB: '&'} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + @Output('outputAttrA') outputA: EventEmitter; + @Output() outputB: EventEmitter; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + + | Outside: {{ dataA }}, {{ dataB }} + ` + }) + class Ng2Component { + dataA = 'foo'; + dataB = 'bar'; + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + var ng1 = element.querySelector('ng1'); + var ng1Controller = angular.element(ng1).controller('ng1'); + + expect(multiTrim(element.textContent)).toBe('Inside: - | Outside: foo, bar'); + + ng1Controller.outputA('baz'); + ng1Controller.outputB('qux'); + tick(); + + expect(multiTrim(element.textContent)).toBe('Inside: - | Outside: baz, qux'); + }); + })); + + it('should bind properties, events', fakeAsync(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: ` + Hello {{ $ctrl.fullName }}; + A: {{ $ctrl.modelA }}; + B: {{ $ctrl.modelB }}; + C: {{ $ctrl.modelC }} + `, + bindings: {fullName: '@', modelA: ' { + if (v === 'Savkin') { + this.modelB = 'SAVKIN'; + this.event('WORKS'); + + // Should not update because `modelA: '; + @Input() modelC: any; + @Output() modelCChange: EventEmitter; + @Output() event: EventEmitter; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + + | + | + {{ event }} - {{ last }}, {{ first }}, {{ city }} + ` + }) + class Ng2Component { + first = 'Victor'; + last = 'Savkin'; + city = 'SF'; + event = '?'; + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(element.textContent)) + .toBe( + 'Hello Savkin, Victor, SF; A: VICTOR; B: SAVKIN; C: sf | ' + + 'Hello TEST; A: First; B: Last; C: City | ' + + 'WORKS - SAVKIN, Victor, SF'); + + // Detect changes + tick(); + + expect(multiTrim(element.textContent)) + .toBe( + 'Hello SAVKIN, Victor, SF; A: VICTOR; B: SAVKIN; C: sf | ' + + 'Hello TEST; A: First; B: Last; C: City | ' + + 'WORKS - SAVKIN, Victor, SF'); + }); + })); + + it('should bind optional properties', fakeAsync(() => { + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: 'Inside: {{ $ctrl.inputA.value }}, {{ $ctrl.inputB }}', + bindings: + {inputA: '=?inputAttrA', inputB: '=?', outputA: '&?outputAttrA', outputB: '&?'} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + @Input('inputAttrA') inputA: string; + @Output('inputAttrAChange') inputAChange: EventEmitter; + @Input() inputB: string; + @Output() inputBChange: EventEmitter; + @Output('outputAttrA') outputA: EventEmitter; + @Output() outputB: EventEmitter; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + | + | + | + | + Outside: {{ dataA.value }}, {{ dataB.value }} + ` + }) + class Ng2Component { + dataA = {value: 'foo'}; + dataB = {value: 'bar'}; + + updateDataB(value: any) { this.dataB.value = value; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + const $rootScope = adapter.$injector.get('$rootScope') as angular.IRootScopeService; + + var ng1s = element.querySelectorAll('ng1'); + var ng1Controller0 = angular.element(ng1s[0]).controller('ng1'); + var ng1Controller1 = angular.element(ng1s[1]).controller('ng1'); + var ng1Controller2 = angular.element(ng1s[2]).controller('ng1'); + + expect(multiTrim(element.textContent)) + .toBe( + 'Inside: foo, bar | Inside: , Bar | Inside: , | Inside: , | Outside: foo, bar'); + + ng1Controller0.inputA.value = 'baz'; + ng1Controller0.inputB = 'qux'; + tick(); + + expect(multiTrim(element.textContent)) + .toBe( + 'Inside: baz, qux | Inside: , Bar | Inside: , | Inside: , | Outside: baz, qux'); + + ng1Controller1.outputA({value: 'foo again'}); + ng1Controller2.outputB('bar again'); + $rootScope.$apply(); + tick(); + + expect(ng1Controller0.inputA).toEqual({value: 'foo again'}); + expect(ng1Controller0.inputB).toEqual('bar again'); + expect(multiTrim(element.textContent)) + .toBe( + 'Inside: foo again, bar again | Inside: , Bar | Inside: , | Inside: , | ' + + 'Outside: foo again, bar again'); + }); + })); + + it('should bind properties, events to scope when bindToController is not used', + fakeAsync(() => { + // Define `ng1Directive` + const ng1Directive: angular.IDirective = { + template: '{{ someText }} - Data: {{ inputA }} - Length: {{ inputA.length }}', + scope: {inputA: '=', outputA: '&'}, + controller: function($scope: angular.IScope) { + $scope['someText'] = 'ng1'; + this.$scope = $scope; + } + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: '[ng1]'}) + class Ng1ComponentFacade extends UpgradeComponent { + @Input() inputA: string; + @Output() inputAChange: EventEmitter; + @Output() outputA: EventEmitter; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` +
| + {{ someText }} - Data: {{ dataA }} - Length: {{ dataA.length }} + ` + }) + class Ng2Component { + someText = 'ng2'; + dataA = [1, 2, 3]; + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .directive('ng1', () => ng1Directive) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + const $rootScope = adapter.$injector.get('$rootScope') as angular.IRootScopeService; + + var ng1 = element.querySelector('[ng1]'); + var ng1Controller = angular.element(ng1).controller('ng1'); + + expect(multiTrim(element.textContent)) + .toBe('ng1 - Data: [1,2,3] - Length: 3 | ng2 - Data: 1,2,3 - Length: 3'); + + ng1Controller.$scope.inputA = [4, 5]; + tick(); + + expect(multiTrim(element.textContent)) + .toBe('ng1 - Data: [4,5] - Length: 2 | ng2 - Data: 4,5 - Length: 2'); + + ng1Controller.$scope.outputA(6); + tick(); + $rootScope.$apply(); + + expect(ng1Controller.$scope.inputA).toEqual([4, 5, 6]); + expect(multiTrim(element.textContent)) + .toBe('ng1 - Data: [4,5,6] - Length: 3 | ng2 - Data: 4,5,6 - Length: 3'); + }); + })); + }); + + describe('controller', () => { + it('should support `controllerAs`', async(() => { + // Define `ng1Directive` + const ng1Directive: angular.IDirective = { + template: + '{{ vm.scope }}; {{ vm.isClass }}; {{ vm.hasElement }}; {{ vm.isPublished() }}', + scope: true, + controllerAs: 'vm', + controller: class { + hasElement: string; isClass: string; scope: string; + + constructor(public $element: angular.IAugmentedJQuery, $scope: angular.IScope) { + this.hasElement = $element[0].nodeName; + this.scope = $scope.$parent.$parent === $scope.$root ? 'scope' : 'wrong-scope'; + + this.verifyIAmAClass(); + } + + isPublished() { + return this.$element.controller('ng1') === this ? 'published' : 'not-published'; + } + + verifyIAmAClass() { this.isClass = 'isClass'; } + } + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .directive('ng1', () => ng1Directive) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(document.body.textContent)).toBe('scope; isClass; NG1; published'); + }); + })); + + it('should support `bindToController` (boolean)', async(() => { + // Define `ng1Directive` + const ng1DirectiveA: angular.IDirective = { + template: 'Scope: {{ title }}; Controller: {{ $ctrl.title }}', + scope: {title: '@'}, + bindToController: false, + controllerAs: '$ctrl', + controller: class {} + }; + + const ng1DirectiveB: angular.IDirective = { + template: 'Scope: {{ title }}; Controller: {{ $ctrl.title }}', + scope: {title: '@'}, + bindToController: true, + controllerAs: '$ctrl', + controller: class {} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1A'}) + class Ng1ComponentAFacade extends UpgradeComponent { + @Input() title: string; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1A', elementRef, injector); + } + } + + @Directive({selector: 'ng1B'}) + class Ng1ComponentBFacade extends UpgradeComponent { + @Input() title: string; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1B', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + | + + ` + }) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .directive('ng1A', () => ng1DirectiveA) + .directive('ng1B', () => ng1DirectiveB) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(document.body.textContent)) + .toBe('Scope: WORKS; Controller: | Scope: ; Controller: WORKS'); + }); + })); + + it('should support `bindToController` (object)', async(() => { + // Define `ng1Directive` + const ng1Directive: angular.IDirective = { + template: '{{ $ctrl.title }}', + scope: {}, + bindToController: {title: '@'}, + controllerAs: '$ctrl', + controller: class {} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + @Input() title: string; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + dataA = 'foo'; + dataB = 'bar'; + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .directive('ng1', () => ng1Directive) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(document.body.textContent)).toBe('WORKS'); + }); + })); + + it('should support `controller` as string', async(() => { + // Define `ng1Directive` + const ng1Directive: angular.IDirective = { + template: '{{ $ctrl.title }} {{ $ctrl.text }}', + scope: {title: '@'}, + bindToController: true, + controller: 'Ng1Controller as $ctrl' + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + @Input() title: string; + + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .controller('Ng1Controller', class { text = 'GREAT'; }) + .directive('ng1', () => ng1Directive) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(document.body.textContent)).toBe('WORKS GREAT'); + }); + })); + }); + + // NOT YET SUPPORTED + xdescribe( + 'require', + () => { + // it('should support single require in linking fn', async(() => { + // const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + // const ng1Module = angular.module('ng1', []); + + // const ng1 = ($rootScope: any /** TODO #9100 */) => { + // return { + // scope: {title: '@'}, + // bindToController: true, + // template: '{{ctl.status}}', + // require: 'ng1', + // controllerAs: 'ctrl', + // controller: Class({constructor: function() { this.status = 'WORKS'; }}), + // link: function( + // scope: any /** TODO #9100 */, element: any /** TODO #9100 */, + // attrs: any /** TODO #9100 */, linkController: any /** TODO #9100 */) { + // expect(scope.$root).toEqual($rootScope); + // expect(element[0].nodeName).toEqual('NG1'); + // expect(linkController.status).toEqual('WORKS'); + // scope.ctl = linkController; + // } + // }; + // }; + // ng1Module.directive('ng1', ng1); + + // const Ng2 = Component({selector: 'ng2', template: ''}).Class({ + // constructor: function() {} + // }); + + // const Ng2Module = NgModule({ + // declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + // imports: [BrowserModule], + // schemas: [NO_ERRORS_SCHEMA], + // }).Class({constructor: function() {}}); + + // ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); + // const element = html(`
`); + // adapter.bootstrap(element, ['ng1']).ready((ref) => { + // expect(multiTrim(document.body.textContent)).toEqual('WORKS'); + // ref.dispose(); + // }); + // })); + + // it('should support array require in linking fn', async(() => { + // const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + // const ng1Module = angular.module('ng1', []); + + // const parent = () => { + // return {controller: Class({constructor: function() { this.parent = 'PARENT'; + // }})}; + // }; + // const ng1 = () => { + // return { + // scope: {title: '@'}, + // bindToController: true, + // template: '{{parent.parent}}:{{ng1.status}}', + // require: ['ng1', '^parent', '?^^notFound'], + // controllerAs: 'ctrl', + // controller: Class({constructor: function() { this.status = 'WORKS'; }}), + // link: function( + // scope: any /** TODO #9100 */, element: any /** TODO #9100 */, + // attrs: any /** TODO #9100 */, linkControllers: any /** TODO #9100 */) { + // 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); + + // const Ng2 = Component({selector: 'ng2', template: ''}).Class({ + // constructor: function() {} + // }); + + // const Ng2Module = NgModule({ + // declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + // imports: [BrowserModule], + // schemas: [NO_ERRORS_SCHEMA], + // }).Class({constructor: function() {}}); + + // ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); + // const element = html(`
`); + // adapter.bootstrap(element, ['ng1']).ready((ref) => { + // expect(multiTrim(document.body.textContent)).toEqual('PARENT:WORKS'); + // ref.dispose(); + // }); + // })); + }); + + describe('lifecycle hooks', () => { + xit('should call `$onChanges()` on controller', () => {}); + xit('should call `$onChanges()` on scope', () => {}); + + it('should call `$onInit()` on controller', async(() => { + // Define `ng1Directive` + const ng1DirectiveA: angular.IDirective = { + template: 'Called: {{ called }}', + bindToController: false, + controller: class { + constructor(private $scope: angular.IScope) { $scope['called'] = 'no'; } + + $onInit() { this.$scope['called'] = 'yes'; } + } + }; + + const ng1DirectiveB: angular.IDirective = { + template: 'Called: {{ called }}', + bindToController: true, + controller: class { + constructor(private $scope: angular.IScope) { $scope['called'] = 'no'; } + + $onInit() { this.$scope['called'] = 'yes'; } + } + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1A'}) + class Ng1ComponentAFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1A', elementRef, injector); + } + } + + @Directive({selector: 'ng1B'}) + class Ng1ComponentBFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1B', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ' | '}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .directive('ng1A', () => ng1DirectiveA) + .directive('ng1B', () => ng1DirectiveB) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(document.body.textContent)).toBe('Called: yes | Called: yes'); + }); + })); + + it('should not call `$onInit()` on scope', async(() => { + // Define `ng1Directive` + const ng1DirectiveA: angular.IDirective = { + template: 'Called: {{ called }}', + bindToController: false, + controller: class { + constructor($scope: angular.IScope) { + $scope['called'] = 'no'; + $scope['$onInit'] = () => $scope['called'] = 'yes'; + } + } + }; + + const ng1DirectiveB: angular.IDirective = { + template: 'Called: {{ called }}', + bindToController: true, + controller: class { + constructor($scope: angular.IScope) { + $scope['called'] = 'no'; + $scope['$onInit'] = () => $scope['called'] = 'yes'; + } + } + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1A'}) + class Ng1ComponentAFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1A', elementRef, injector); + } + } + + @Directive({selector: 'ng1B'}) + class Ng1ComponentBFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1B', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ' | '}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .directive('ng1A', () => ng1DirectiveA) + .directive('ng1B', () => ng1DirectiveB) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + platformBrowserDynamic().bootstrapModule(Ng2Module).then(ref => { + var adapter = ref.injector.get(UpgradeModule) as UpgradeModule; + adapter.bootstrap(element, [ng1Module.name]); + + expect(multiTrim(document.body.textContent)).toBe('Called: no | Called: no'); + }); + })); + + xit('should call `$onPostDigest()` on controller', () => {}); + xit('should not call `$onPostDigest()` on scope', () => {}); + + xit('should call `$onDestroy()` on controller', () => {}); + xit('should not call `$onDestroy()` on scope', () => {}); + }); + + // it('should support ng2 > ng1 > ng2', async(() => { + // const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + // const ng1Module = angular.module('ng1', []); + + // const ng1 = { + // template: 'ng1()', + // }; + // ng1Module.component('ng1', ng1); + + // const Ng2a = Component({selector: 'ng2a', template: 'ng2a()'}).Class({ + // constructor: function() {} + // }); + // ng1Module.directive('ng2a', adapter.downgradeNg2Component(Ng2a)); + + // const Ng2b = + // Component({selector: 'ng2b', template: 'ng2b'}).Class({constructor: function() {}}); + // ng1Module.directive('ng2b', adapter.downgradeNg2Component(Ng2b)); + + // const Ng2Module = NgModule({ + // declarations: [adapter.upgradeNg1Component('ng1'), Ng2a, Ng2b], + // imports: [BrowserModule], + // schemas: [NO_ERRORS_SCHEMA], + // }).Class({constructor: function() {}}); + + // const element = html(`
`); + // adapter.bootstrap(element, ['ng1']).ready((ref) => { + // expect(multiTrim(document.body.textContent)).toEqual('ng2a(ng1(ng2b))'); + // }); + // })); + }); +} \ No newline at end of file diff --git a/modules/@angular/upgrade/test/aot/test_helpers.ts b/modules/@angular/upgrade/test/aot/test_helpers.ts new file mode 100644 index 0000000000..ac9e271549 --- /dev/null +++ b/modules/@angular/upgrade/test/aot/test_helpers.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {PlatformRef, Type} from '@angular/core'; +import {UpgradeModule} from '@angular/upgrade'; +import * as angular from '@angular/upgrade/src/angular_js'; + +export function bootstrap( + platform: PlatformRef, Ng2Module: Type<{}>, element: Element, ng1Module: angular.IModule) { + // We bootstrap the Angular 2 module first; then when it is ready (async) + // We bootstrap the Angular 1 module on the bootstrap element + return platform.bootstrapModule(Ng2Module).then(ref => { + var upgrade = ref.injector.get(UpgradeModule) as UpgradeModule; + upgrade.bootstrap(element, [ng1Module.name]); + return upgrade; + }); +} + +export function html(html: string): Element { + // Don't return `body` itself, because using it as a `$rootElement` for ng1 + // will attach `$injector` to it and that will affect subsequent tests. + const body = document.body; + body.innerHTML = `
${html.trim()}
`; + const div = document.body.firstChild as Element; + + if (div.childNodes.length === 1 && div.firstChild instanceof HTMLElement) { + return div.firstChild; + } + + return div; +} + +export function multiTrim(text: string): string { + return text.replace(/\n/g, '').replace(/\s\s+/g, ' ').trim(); +} diff --git a/scripts/windows/packages.txt b/scripts/windows/packages.txt index 2397cf357a..7b758d3ff6 100644 --- a/scripts/windows/packages.txt +++ b/scripts/windows/packages.txt @@ -11,3 +11,4 @@ platform-browser-dynamic/src platform-server/src platform-webworker/src platform-webworker-dynamic/src +upgrade/src \ No newline at end of file diff --git a/tools/public_api_guard/upgrade/index.d.ts b/tools/public_api_guard/upgrade/index.d.ts index 33ef4ab993..3e82448ee0 100644 --- a/tools/public_api_guard/upgrade/index.d.ts +++ b/tools/public_api_guard/upgrade/index.d.ts @@ -1,3 +1,9 @@ +/** @experimental */ +export declare function downgradeComponent(info: ComponentInfo): angular.IInjectable; + +/** @experimental */ +export declare function downgradeInjectable(token: any): (string | ((i: Injector) => any))[]; + /** @stable */ export declare class UpgradeAdapter { constructor(ng2AppModule: Type, compilerOptions?: CompilerOptions); @@ -19,3 +25,20 @@ export declare class UpgradeAdapterRef { dispose(): void; ready(fn: (upgradeAdapterRef?: UpgradeAdapterRef) => void): void; } + +/** @experimental */ +export declare class UpgradeComponent implements OnInit, OnChanges, DoCheck { + constructor(name: string, elementRef: ElementRef, injector: Injector); + ngDoCheck(): void; + ngOnChanges(changes: SimpleChanges): void; + ngOnInit(): void; +} + +/** @experimental */ +export declare class UpgradeModule { + $injector: angular.IInjectorService; + injector: Injector; + ngZone: NgZone; + constructor(injector: Injector, ngZone: NgZone); + bootstrap(element: Element, modules?: string[], config?: angular.IAngularBootstrapConfig): void; +}