diff --git a/modules/upgrade/src/angular.d.ts b/modules/upgrade/src/angular.d.ts index a7099734c8..3aac7529e2 100644 --- a/modules/upgrade/src/angular.d.ts +++ b/modules/upgrade/src/angular.d.ts @@ -1,6 +1,7 @@ declare namespace angular { function module(prefix: string, dependencies?: string[]); interface IModule { + config(fn: any): IModule; directive(selector: string, factory: any): IModule; value(key: string, value: any): IModule; run(a: any); @@ -13,11 +14,19 @@ declare namespace angular { $watch(expr: any, fn?: (a1?: any, a2?: any) => void); $apply(): any; $apply(exp: string): any; - $apply(exp: (scope: IScope) => any): any; + $apply(exp: Function): any; + $$childTail: IScope; + $$childHead: IScope; + $$nextSibling: IScope; } interface IScope extends IRootScopeService {} interface IAngularBootstrapConfig {} - interface IDirective {} + interface IDirective { + require?: string; + restrict?: string; + scope?: {[key: string]: string}; + link?: Function; + } interface IAttributes { $observe(attr: string, fn: (v: string) => void); } diff --git a/modules/upgrade/src/constants.ts b/modules/upgrade/src/constants.ts new file mode 100644 index 0000000000..27ed30ce06 --- /dev/null +++ b/modules/upgrade/src/constants.ts @@ -0,0 +1,13 @@ +export const NG2_APP_VIEW_MANAGER = 'ng2.AppViewManager'; +export const NG2_COMPILER = 'ng2.Compiler'; +export const NG2_INJECTOR = 'ng2.Injector'; +export const NG2_PROTO_VIEW_REF_MAP = 'ng2.ProtoViewRefMap'; +export const NG2_ZONE = 'ng2.NgZone'; + +export const NG1_REQUIRE_INJECTOR_REF = '$' + NG2_INJECTOR + 'Controller'; +export const NG1_SCOPE = '$scope'; +export const NG1_ROOT_SCOPE = '$rootScope'; +export const NG1_COMPILE = '$compile'; +export const NG1_INJECTOR = '$injector'; +export const NG1_PARSE = '$parse'; +export const REQUIRE_INJECTOR = '^' + NG2_INJECTOR; diff --git a/modules/upgrade/src/ng1_facade.ts b/modules/upgrade/src/ng1_facade.ts new file mode 100644 index 0000000000..d1a257846a --- /dev/null +++ b/modules/upgrade/src/ng1_facade.ts @@ -0,0 +1,165 @@ +import { + Directive, + DoCheck, + ElementRef, + EventEmitter, + Inject, + OnChanges, + SimpleChange, + Type +} from 'angular2/angular2'; +import {NG1_COMPILE, NG1_SCOPE} from './constants'; + +const CAMEL_CASE = /([A-Z])/g; +const INITIAL_VALUE = { + __UNINITIALIZED__: true +}; + + +export class ExportedNg1Component { + type: Type; + inputs: string[] = []; + inputsRename: string[] = []; + outputs: string[] = []; + outputsRename: string[] = []; + propertyOutputs: string[] = []; + checkProperties: string[] = []; + propertyMap: {[name: string]: string} = {}; + + constructor(public name: string) { + var selector = name.replace(CAMEL_CASE, (all, next: string) => '-' + next.toLowerCase()); + var self = this; + this.type = + Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename}) + .Class({ + constructor: [ + new Inject(NG1_COMPILE), + new Inject(NG1_SCOPE), + ElementRef, + function(compile: angular.ICompileService, scope: angular.IScope, + elementRef: ElementRef) { + return new Ng1ComponentFacade(compile, scope, elementRef, self.inputs, + self.outputs, self.propertyOutputs, + self.checkProperties, self.propertyMap); + } + ], + onChanges: function() { /* needs to be here for ng2 to properly detect it */ }, + doCheck: function() { /* needs to be here for ng2 to properly detect it */ } + }); + } + + extractBindings(injector: angular.auto.IInjectorService) { + var directives: angular.IDirective[] = injector.get(this.name + 'Directive'); + if (directives.length > 1) { + throw new Error('Only support single directive definition for: ' + this.name); + } + var directive = directives[0]; + var scope = directive.scope; + if (typeof scope == 'object') { + for (var name in scope) { + if (scope.hasOwnProperty(name)) { + var localName = scope[name]; + var type = localName.charAt(0); + localName = localName.substr(1) || name; + var outputName = 'output_' + name; + var outputNameRename = outputName + ': ' + name; + var inputName = 'input_' + name; + var inputNameRename = inputName + ': ' + name; + switch (type) { + case '=': + this.propertyOutputs.push(outputName); + this.checkProperties.push(localName); + this.outputs.push(outputName); + this.outputsRename.push(outputNameRename); + this.propertyMap[outputName] = localName; + // don't break; let it fall through to '@' + case '@': + this.inputs.push(inputName); + this.inputsRename.push(inputNameRename); + this.propertyMap[inputName] = localName; + break; + case '&': + this.outputs.push(outputName); + this.outputsRename.push(outputNameRename); + this.propertyMap[outputName] = localName; + break; + default: + var json = JSON.stringify(scope); + throw new Error( + `Unexpected mapping '${type}' in '${json}' in '${this.name}' directive.`); + } + } + } + } + } + + static resolve(exportedComponents: {[name: string]: ExportedNg1Component}, + injector: angular.auto.IInjectorService) { + for (var name in exportedComponents) { + if (exportedComponents.hasOwnProperty(name)) { + var exportedComponent = exportedComponents[name]; + exportedComponent.extractBindings(injector); + } + } + } +} + +class Ng1ComponentFacade implements OnChanges, DoCheck { + componentScope: angular.IScope = null; + checkLastValues: any[] = []; + + constructor(compile: angular.ICompileService, scope: angular.IScope, elementRef: ElementRef, + private inputs: string[], private outputs: string[], private propOuts: string[], + private checkProperties: string[], private propertyMap: {[key: string]: string}) { + var chailTail = scope.$$childTail; // remember where the next scope is inserted + compile(elementRef.nativeElement)(scope); + + // If we are first scope take it, otherwise take the next one in list. + this.componentScope = chailTail ? chailTail.$$nextSibling : scope.$$childHead; + + for (var i = 0; i < inputs.length; i++) { + this[inputs[i]] = null; + } + for (var j = 0; j < outputs.length; j++) { + var emitter = this[outputs[j]] = new EventEmitter(); + this.setComponentProperty(outputs[j], ((emitter) => (value) => emitter.next(value))(emitter)); + } + for (var k = 0; k < propOuts.length; k++) { + this[propOuts[k]] = new EventEmitter(); + this.checkLastValues.push(INITIAL_VALUE); + } + } + + onChanges(changes) { + for (var name in changes) { + if (changes.hasOwnProperty(name)) { + var change: SimpleChange = changes[name]; + this.setComponentProperty(name, change.currentValue); + } + } + } + + doCheck() { + var count = 0; + var scope = this.componentScope; + var lastValues = this.checkLastValues; + var checkProperties = this.checkProperties; + for (var i = 0; i < checkProperties.length; i++) { + var value = scope[checkProperties[i]]; + var last = lastValues[i]; + if (value !== last) { + if (typeof value == 'number' && isNaN(value) && typeof last == 'number' && isNaN(last)) { + // ignore because NaN != NaN + } else { + var eventEmitter: EventEmitter = this[this.propOuts[i]]; + eventEmitter.next(lastValues[i] = value); + } + } + } + return count; + } + + setComponentProperty(name: string, value: any) { + this.componentScope[this.propertyMap[name]] = value; + } +} diff --git a/modules/upgrade/src/ng2_facade.ts b/modules/upgrade/src/ng2_facade.ts new file mode 100644 index 0000000000..63baa0dcdd --- /dev/null +++ b/modules/upgrade/src/ng2_facade.ts @@ -0,0 +1,144 @@ +import { + bind, + AppViewManager, + ChangeDetectorRef, + HostViewRef, + Injector, + ProtoViewRef, + SimpleChange +} from 'angular2/angular2'; +import {NG1_SCOPE} from './constants'; +import {ComponentInfo} from './metadata'; + +const INITIAL_VALUE = { + __UNINITIALIZED__: true +}; + +export class Ng2ComponentFacade { + component: any = null; + inputChangeCount: number = 0; + inputChanges: {[key: string]: SimpleChange} = null; + hostViewRef: HostViewRef = null; + changeDetector: ChangeDetectorRef = null; + componentScope: angular.IScope; + + constructor(private id: string, private info: ComponentInfo, + private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes, + private scope: angular.IScope, private parentInjector: Injector, + private parse: angular.IParseService, private viewManager: AppViewManager, + private protoView: ProtoViewRef) { + this.componentScope = scope.$new(); + } + + bootstrapNg2() { + var childInjector = + this.parentInjector.resolveAndCreateChild([bind(NG1_SCOPE).toValue(this.componentScope)]); + this.hostViewRef = + this.viewManager.createRootHostView(this.protoView, '#' + this.id, childInjector); + var hostElement = this.viewManager.getHostElement(this.hostViewRef); + this.changeDetector = this.hostViewRef.changeDetectorRef; + this.component = this.viewManager.getComponent(hostElement); + } + + setupInputs() { + var attrs = this.attrs; + var inputs = this.info.inputs; + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + var expr = null; + if (attrs.hasOwnProperty(input.attr)) { + var observeFn = ((prop) => { + var prevValue = INITIAL_VALUE; + return (value) => { + if (this.inputChanges !== null) { + this.inputChangeCount++; + this.inputChanges[prop] = + new Ng1Change(value, prevValue === INITIAL_VALUE ? value : prevValue); + prevValue = value; + } + this.component[prop] = value; + } + })(input.prop); + attrs.$observe(input.attr, observeFn); + } else if (attrs.hasOwnProperty(input.bindAttr)) { + expr = attrs[input.bindAttr]; + } else if (attrs.hasOwnProperty(input.bracketAttr)) { + expr = attrs[input.bracketAttr]; + } else if (attrs.hasOwnProperty(input.bindonAttr)) { + expr = attrs[input.bindonAttr]; + } else if (attrs.hasOwnProperty(input.bracketParenAttr)) { + expr = attrs[input.bracketParenAttr]; + } + if (expr != null) { + var watchFn = ((prop) => (value, prevValue) => { + if (this.inputChanges != null) { + this.inputChangeCount++; + this.inputChanges[prop] = new Ng1Change(prevValue, value); + } + this.component[prop] = value; + })(input.prop); + this.componentScope.$watch(expr, watchFn); + } + } + + var prototype = this.info.type.prototype; + if (prototype && prototype.onChanges) { + // Detect: OnChanges interface + this.inputChanges = {}; + this.componentScope.$watch(() => this.inputChangeCount, () => { + var inputChanges = this.inputChanges; + this.inputChanges = {}; + this.component.onChanges(inputChanges); + }); + } + this.componentScope.$watch(() => this.changeDetector.detectChanges()); + } + + setupOutputs() { + var attrs = this.attrs; + var outputs = this.info.outputs; + for (var j = 0; j < outputs.length; j++) { + var output = outputs[j]; + var expr = null; + var assignExpr = false; + if (attrs.hasOwnProperty(output.onAttr)) { + expr = attrs[output.onAttr]; + } else if (attrs.hasOwnProperty(output.parenAttr)) { + expr = attrs[output.parenAttr]; + } else if (attrs.hasOwnProperty(output.bindonAttr)) { + expr = attrs[output.bindonAttr]; + assignExpr = true; + } else if (attrs.hasOwnProperty(output.bracketParenAttr)) { + expr = attrs[output.bracketParenAttr]; + assignExpr = true; + } + + if (expr != null && assignExpr != null) { + var getter = this.parse(expr); + var setter = getter.assign; + if (assignExpr && !setter) { + throw new Error(`Expression '${expr}' is not assignable!`); + } + var emitter = this.component[output.prop]; + if (emitter) { + emitter.observer({ + next: assignExpr ? ((setter) => (value) => setter(this.scope, value))(setter) : + ((getter) => (value) => getter(this.scope, {$event: value}))(getter) + }); + } else { + throw new Error(`Missing emitter '${output.prop}' on component '${this.info.selector}'!`); + } + } + } + } + + registerCleanup() { + this.element.bind('$remove', () => this.viewManager.destroyRootHostView(this.hostViewRef)); + } +} + +class Ng1Change implements SimpleChange { + constructor(public previousValue: any, public currentValue: any) {} + + isFirstChange(): boolean { return this.previousValue === this.currentValue; } +} diff --git a/modules/upgrade/src/upgrade_module.ts b/modules/upgrade/src/upgrade_module.ts index c7da2c8775..3f9abb433f 100644 --- a/modules/upgrade/src/upgrade_module.ts +++ b/modules/upgrade/src/upgrade_module.ts @@ -1,63 +1,55 @@ /// import { - platform, - ComponentRef, bind, - Directive, - Component, - Inject, - View, - Type, - PlatformRef, + platform, ApplicationRef, - ChangeDetectorRef, AppViewManager, - NgZone, - Injector, Compiler, + Injector, + NgZone, + PlatformRef, ProtoViewRef, - ElementRef, - HostViewRef, - ViewRef, - SimpleChange + Type } from 'angular2/angular2'; import {applicationDomBindings} from 'angular2/src/core/application_common'; -import {applicationCommonBindings} from '../../angular2/src/core/application_ref'; +import {applicationCommonBindings} from 'angular2/src/core/application_ref'; import {compilerBindings} from 'angular2/src/core/compiler/compiler'; import {getComponentInfo, ComponentInfo} from './metadata'; import {onError} from './util'; -export const INJECTOR = 'ng2.Injector'; -export const APP_VIEW_MANAGER = 'ng2.AppViewManager'; -export const NG2_COMPILER = 'ng2.Compiler'; -export const NG2_ZONE = 'ng2.NgZone'; -export const PROTO_VIEW_REF_MAP = 'ng2.ProtoViewRefMap'; - -const NG1_REQUIRE_INJECTOR_REF = '$' + INJECTOR + 'Controller'; -const NG1_SCOPE = '$scope'; -const NG1_COMPILE = '$compile'; -const NG1_INJECTOR = '$injector'; -const NG1_PARSE = '$parse'; -const REQUIRE_INJECTOR = '^' + INJECTOR; +import { + NG1_COMPILE, + NG1_INJECTOR, + NG1_PARSE, + NG1_ROOT_SCOPE, + NG1_REQUIRE_INJECTOR_REF, + NG1_SCOPE, + NG2_APP_VIEW_MANAGER, + NG2_COMPILER, + NG2_INJECTOR, + NG2_PROTO_VIEW_REF_MAP, + NG2_ZONE, + REQUIRE_INJECTOR +} from './constants'; +import {Ng2ComponentFacade} from './ng2_facade'; +import {ExportedNg1Component} from './ng1_facade'; var moduleCount: number = 0; -const CAMEL_CASE = /([A-Z])/g; -var INITIAL_VALUE = {}; export function createUpgradeModule(): UpgradeModule { var prefix = `NG2_UPGRADE_m${moduleCount++}_`; return new UpgradeModule(prefix, angular.module(prefix, [])); } - export class UpgradeModule { - componentTypes: Array = []; + importedNg2Components: Type[] = []; + exportedNg1Components: {[name: string]: ExportedNg1Component} = {} constructor(public idPrefix: string, public ng1Module: angular.IModule) {} importNg2Component(type: Type): UpgradeModule { - this.componentTypes.push(type); + this.importedNg2Components.push(type); var info: ComponentInfo = getComponentInfo(type); var factory: Function = ng1ComponentDirective(info, `${this.idPrefix}${info.selector}_c`); this.ng1Module.directive(info.selector, factory); @@ -65,18 +57,11 @@ export class UpgradeModule { } exportAsNg2Component(name: string): Type { - return Directive({ - selector: name.replace(CAMEL_CASE, (all, next: string) => '-' + next.toLowerCase()) - }) - .Class({ - constructor: [ - new Inject(NG1_COMPILE), - new Inject(NG1_SCOPE), - ElementRef, - function(compile: angular.ICompileService, scope: angular.IScope, - elementRef: ElementRef) { compile(elementRef.nativeElement)(scope); } - ] - }); + if (this.exportedNg1Components.hasOwnProperty(name)) { + return this.exportedNg1Components[name].type; + } else { + return (this.exportedNg1Components[name] = new ExportedNg1Component(name)).type; + } } bootstrap(element: Element, modules?: any[], @@ -96,42 +81,72 @@ export class UpgradeModule { var injector: Injector = applicationRef.injector; var ngZone: NgZone = injector.get(NgZone); var compiler: Compiler = injector.get(Compiler); - this.compileNg2Components(compiler).then((protoViewRefMap: ProtoViewRefMap) => { - ngZone.run(() => { - this.ng1Module.value(INJECTOR, injector) - .value(NG2_ZONE, ngZone) - .value(NG2_COMPILER, compiler) - .value(PROTO_VIEW_REF_MAP, protoViewRefMap) - .value(APP_VIEW_MANAGER, injector.get(AppViewManager)) - .run([ - '$injector', - '$rootScope', - (injector: angular.auto.IInjectorService, rootScope: angular.IRootScopeService) => { - ng1Injector = injector; - ngZone.overrideOnTurnDone(() => rootScope.$apply()); - } - ]); + var delayApplyExps: Function[] = []; + var original$applyFn: Function; + var rootScopePrototype: any; + var rootScope: angular.IRootScopeService; + var protoViewRefMap: ProtoViewRefMap = {}; + ngZone.run(() => { + this.ng1Module.value(NG2_INJECTOR, injector) + .value(NG2_ZONE, ngZone) + .value(NG2_COMPILER, compiler) + .value(NG2_PROTO_VIEW_REF_MAP, protoViewRefMap) + .value(NG2_APP_VIEW_MANAGER, injector.get(AppViewManager)) + .config([ + '$provide', + (provide) => { + provide.decorator(NG1_ROOT_SCOPE, [ + '$delegate', + function(rootScopeDelegate: angular.IRootScopeService) { + rootScopePrototype = rootScopeDelegate.constructor.prototype; + if (rootScopePrototype.hasOwnProperty('$apply')) { + original$applyFn = rootScopePrototype.$apply; + rootScopePrototype.$apply = (exp) => delayApplyExps.push(exp); + } else { + throw new Error("Failed to find '$apply' on '$rootScope'!"); + } + return rootScope = rootScopeDelegate; + } + ]); + } + ]) + .run([ + '$injector', + '$rootScope', + (injector: angular.auto.IInjectorService, rootScope: angular.IRootScopeService) => { + ng1Injector = injector; + ngZone.overrideOnTurnDone(() => rootScope.$apply()); + ExportedNg1Component.resolve(this.exportedNg1Components, injector); + } + ]); - modules = modules ? [].concat(modules) : []; - modules.push(this.idPrefix); - angular.element(element).data(NG1_REQUIRE_INJECTOR_REF, injector); - angular.bootstrap(element, modules, config); - - upgrade.readyFn && upgrade.readyFn(); - }); + modules = modules ? [].concat(modules) : []; + modules.push(this.idPrefix); + angular.element(element).data(NG1_REQUIRE_INJECTOR_REF, injector); + angular.bootstrap(element, modules, config); }); + this.compileNg2Components(compiler, protoViewRefMap) + .then((protoViewRefMap: ProtoViewRefMap) => { + ngZone.run(() => { + rootScopePrototype.$apply = original$applyFn; // restore original $apply + while (delayApplyExps.length) { + rootScope.$apply(delayApplyExps.shift()); + } + upgrade.readyFn && upgrade.readyFn(); + }); + }); return upgrade; } - private compileNg2Components(compiler: Compiler): Promise { + private compileNg2Components(compiler: Compiler, + protoViewRefMap: ProtoViewRefMap): Promise { var promises: Array> = []; - var types = this.componentTypes; + var types = this.importedNg2Components; for (var i = 0; i < types.length; i++) { promises.push(compiler.compileInHost(types[i])); } return Promise.all(promises).then((protoViews: Array) => { - var protoViewRefMap: ProtoViewRefMap = {}; - var types = this.componentTypes; + var types = this.importedNg2Components; for (var i = 0; i < protoViews.length; i++) { protoViewRefMap[getComponentInfo(types[i]).selector] = protoViews[i]; } @@ -145,7 +160,7 @@ interface ProtoViewRefMap { } function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function { - directiveFactory.$inject = [PROTO_VIEW_REF_MAP, APP_VIEW_MANAGER, NG1_PARSE]; + directiveFactory.$inject = [NG2_PROTO_VIEW_REF_MAP, NG2_APP_VIEW_MANAGER, NG1_PARSE]; function directiveFactory(protoViewRefMap: ProtoViewRefMap, viewManager: AppViewManager, parse: angular.IParseService): angular.IDirective { var protoView: ProtoViewRef = protoViewRefMap[info.selector]; @@ -170,136 +185,6 @@ function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function return directiveFactory; } -class Ng2ComponentFacade { - component: any = null; - inputChangeCount: number = 0; - inputChanges: {[key: string]: SimpleChange} = null; - hostViewRef: HostViewRef = null; - changeDetector: ChangeDetectorRef = null; - componentScope: angular.IScope; - - constructor(private id: string, private info: ComponentInfo, - private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes, - private scope: angular.IScope, private parentInjector: Injector, - private parse: angular.IParseService, private viewManager: AppViewManager, - private protoView: ProtoViewRef) { - this.componentScope = scope.$new(); - } - - bootstrapNg2() { - var childInjector = - this.parentInjector.resolveAndCreateChild([bind(NG1_SCOPE).toValue(this.componentScope)]); - this.hostViewRef = - this.viewManager.createRootHostView(this.protoView, '#' + this.id, childInjector); - var hostElement = this.viewManager.getHostElement(this.hostViewRef); - this.changeDetector = this.hostViewRef.changeDetectorRef; - this.component = this.viewManager.getComponent(hostElement); - } - - setupInputs() { - var attrs = this.attrs; - var inputs = this.info.inputs; - for (var i = 0; i < inputs.length; i++) { - var input = inputs[i]; - var expr = null; - if (attrs.hasOwnProperty(input.attr)) { - var observeFn = ((prop) => { - var prevValue = INITIAL_VALUE; - return (value) => { - if (this.inputChanges !== null) { - this.inputChangeCount++; - this.inputChanges[prop] = - new Ng1Change(value, prevValue === INITIAL_VALUE ? value : prevValue); - prevValue = value; - } - this.component[prop] = value; - }; - })(input.prop); - attrs.$observe(input.attr, observeFn); - } else if (attrs.hasOwnProperty(input.bindAttr)) { - expr = attrs[input.bindAttr]; - } else if (attrs.hasOwnProperty(input.bracketAttr)) { - expr = attrs[input.bracketAttr]; - } else if (attrs.hasOwnProperty(input.bindonAttr)) { - expr = attrs[input.bindonAttr]; - } else if (attrs.hasOwnProperty(input.bracketParenAttr)) { - expr = attrs[input.bracketParenAttr]; - } - if (expr != null) { - var watchFn = ((prop) => (value, prevValue) => { - if (this.inputChanges != null) { - this.inputChangeCount++; - this.inputChanges[prop] = new Ng1Change(prevValue, value); - } - this.component[prop] = value; - })(input.prop); - this.componentScope.$watch(expr, watchFn); - } - } - - var prototype = this.info.type.prototype; - if (prototype && prototype.onChanges) { - // Detect: OnChanges interface - this.inputChanges = {}; - this.componentScope.$watch(() => this.inputChangeCount, () => { - var inputChanges = this.inputChanges; - this.inputChanges = {}; - this.component.onChanges(inputChanges); - }); - } - this.componentScope.$watch(() => this.changeDetector.detectChanges()); - } - - setupOutputs() { - var attrs = this.attrs; - var outputs = this.info.outputs; - for (var j = 0; j < outputs.length; j++) { - var output = outputs[j]; - var expr = null; - var assignExpr = false; - if (attrs.hasOwnProperty(output.onAttr)) { - expr = attrs[output.onAttr]; - } else if (attrs.hasOwnProperty(output.parenAttr)) { - expr = attrs[output.parenAttr]; - } else if (attrs.hasOwnProperty(output.bindonAttr)) { - expr = attrs[output.bindonAttr]; - assignExpr = true; - } else if (attrs.hasOwnProperty(output.bracketParenAttr)) { - expr = attrs[output.bracketParenAttr]; - assignExpr = true; - } - - if (expr != null && assignExpr != null) { - var getter = this.parse(expr); - var setter = getter.assign; - if (assignExpr && !setter) { - throw new Error(`Expression '${expr}' is not assignable!`); - } - var emitter = this.component[output.prop]; - if (emitter) { - emitter.observer({ - next: assignExpr ? ((setter) => (value) => setter(this.scope, value))(setter) : - ((getter) => (value) => getter(this.scope, {$event: value}))(getter) - }); - } else { - throw new Error(`Missing emitter '${output.prop}' on component '${this.info.selector}'!`); - } - } - } - } - - registerCleanup() { - this.element.bind('$remove', () => this.viewManager.destroyRootHostView(this.hostViewRef)); - } -} - -export class Ng1Change implements SimpleChange { - constructor(public previousValue: any, public currentValue: any) {} - - isFirstChange(): boolean { return this.previousValue === this.currentValue; } -} - - export class UpgradeRef { readyFn: Function; diff --git a/modules/upgrade/test/integration_spec.ts b/modules/upgrade/test/integration_spec.ts index a0e6cb5a8a..d6c4d04d4c 100644 --- a/modules/upgrade/test/integration_spec.ts +++ b/modules/upgrade/test/integration_spec.ts @@ -205,6 +205,60 @@ export function main() { })); }); + + describe('binding from ng2 to ng1', () => { + it('should bind properties, events', inject([AsyncTestCompleter], (async) => { + var upgrMod = createUpgradeModule(); + var ng1 = function() { + return { + template: 'Hello {{fullName}}; A: {{dataA}}; B: {{dataB}}; | ', + scope: {fullName: '@', modelA: '=dataA', modelB: '=dataB', event: '&'}, + link: function(scope) { + scope.$watch('dataB', (v) => { + if (v == 'Savkin') { + scope.dataB = 'SAVKIN'; + scope.event('WORKS'); + + // Should not update becaus [model-a] is uni directional + scope.dataA = 'VICTOR'; + } + }) + } + } + }; + upgrMod.ng1Module.directive('ng1', ng1); + var ng2 = + Component({selector: 'ng2'}) + .View({ + template: + '' + + '' + + '{{event}}-{{last}}, {{first}}', + directives: [upgrMod.exportAsNg2Component('ng1')] + }) + .Class({ + constructor: function() { + this.first = 'Victor'; + this.last = 'Savkin'; + this.event = '?'; + } + }); + upgrMod.importNg2Component(ng2); + var element = html(`
`); + upgrMod.bootstrap(element).ready(() => { + // we need to do setTimeout, because the EventEmitter uses setTimeout to schedule + // events, and so without this we would not see the events processed. + setTimeout(() => { + expect(multiTrim(document.body.textContent)) + .toEqual( + "Hello SAVKIN, Victor; A: VICTOR; B: SAVKIN; | Hello TEST; A: First; B: Last; | WORKS-SAVKIN, Victor"); + async.done(); + }, 0); + }); + })); + }); + }); }