diff --git a/modules/angular2/angular2.js b/modules/angular2/angular2.js index 41f8bda3fa..218f94df14 100644 --- a/modules/angular2/angular2.js +++ b/modules/angular2/angular2.js @@ -3,3 +3,4 @@ export * from './core'; export * from './annotations'; export * from './directives'; export * from './forms'; +export {Observable, EventEmitter} from 'angular2/src/facade/async'; diff --git a/modules/angular2/src/core/annotations/annotations.js b/modules/angular2/src/core/annotations/annotations.js index a803f1bc8a..bfc4cac67a 100644 --- a/modules/angular2/src/core/annotations/annotations.js +++ b/modules/angular2/src/core/annotations/annotations.js @@ -360,6 +360,30 @@ export class Directive extends Injectable { */ properties:any; // StringMap + /** + * Enumerates the set of emitted events. + * + * ## Syntax + * + * ``` + * @Component({ + * events: ['status-change'] + * }) + * class TaskComponent { + * statusChange:EventEmitter; + * + * constructor() { + * this.complete = new EventEmitter(); + * } + * + * onComplete() { + * this.statusChange.next("completed"); + * } + * } + * ``` + */ + events:List; + /** * Specifies which DOM hostListeners a directive listens to. * @@ -426,11 +450,13 @@ export class Directive extends Injectable { constructor({ selector, properties, + events, hostListeners, lifecycle }:{ selector:string, properties:any, + events:List, hostListeners: any, lifecycle:List }={}) @@ -438,6 +464,7 @@ export class Directive extends Injectable { super(); this.selector = selector; this.properties = properties; + this.events = events; this.hostListeners = hostListeners; this.lifecycle = lifecycle; } @@ -551,6 +578,7 @@ export class Component extends Directive { constructor({ selector, properties, + events, hostListeners, injectables, lifecycle, @@ -558,6 +586,7 @@ export class Component extends Directive { }:{ selector:string, properties:Object, + events:List, hostListeners:Object, injectables:List, lifecycle:List, @@ -567,6 +596,7 @@ export class Component extends Directive { super({ selector: selector, properties: properties, + events: events, hostListeners: hostListeners, lifecycle: lifecycle }); @@ -634,12 +664,14 @@ export class DynamicComponent extends Directive { constructor({ selector, properties, + events, hostListeners, injectables, lifecycle }:{ selector:string, properties:Object, + events:List, hostListeners:Object, injectables:List, lifecycle:List @@ -647,6 +679,7 @@ export class DynamicComponent extends Directive { super({ selector: selector, properties: properties, + events: events, hostListeners: hostListeners, lifecycle: lifecycle }); @@ -727,12 +760,14 @@ export class Decorator extends Directive { constructor({ selector, properties, + events, hostListeners, lifecycle, compileChildren = true, }:{ selector:string, properties:any, + events:List, hostListeners:any, lifecycle:List, compileChildren:boolean @@ -741,6 +776,7 @@ export class Decorator extends Directive { super({ selector: selector, properties: properties, + events: events, hostListeners: hostListeners, lifecycle: lifecycle }); @@ -846,17 +882,20 @@ export class Viewport extends Directive { constructor({ selector, properties, + events, hostListeners, lifecycle }:{ selector:string, properties:any, + events:List, lifecycle:List }={}) { super({ selector: selector, properties: properties, + events: events, hostListeners: hostListeners, lifecycle: lifecycle }); diff --git a/modules/angular2/src/core/annotations/di.js b/modules/angular2/src/core/annotations/di.js index 5fc2887af9..2bcc97f0c9 100644 --- a/modules/angular2/src/core/annotations/di.js +++ b/modules/angular2/src/core/annotations/di.js @@ -1,34 +1,11 @@ import {CONST} from 'angular2/src/facade/lang'; import {DependencyAnnotation} from 'angular2/di'; -/** - * Specifies that a function for emitting events should be injected. - * - * NOTE: This is changing pre 1.0. - * - * The directive can inject an emitter function that would emit events onto the directive host element. - * - * @exportedAs angular2/annotations - */ -export class EventEmitter extends DependencyAnnotation { - eventName: string; - - @CONST() - constructor(eventName) { - super(); - this.eventName = eventName; - } - - get token() { - return Function; - } -} - /** * Specifies that a function for setting host properties should be injected. * * NOTE: This is changing pre 1.0. - * + * * The directive can inject a property setter that would allow setting this property on the host element. * * @exportedAs angular2/annotations diff --git a/modules/angular2/src/core/compiler/element_injector.js b/modules/angular2/src/core/compiler/element_injector.js index 7748a9aac2..36d63204b6 100644 --- a/modules/angular2/src/core/compiler/element_injector.js +++ b/modules/angular2/src/core/compiler/element_injector.js @@ -3,13 +3,14 @@ import {Math} from 'angular2/src/facade/math'; import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; import {Injector, Key, Dependency, bind, Binding, ResolvedBinding, NoProviderError, ProviderError, CyclicDependencyError} from 'angular2/di'; import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility'; -import {EventEmitter, PropertySetter, Attribute, Query} from 'angular2/src/core/annotations/di'; +import {PropertySetter, Attribute, Query} from 'angular2/src/core/annotations/di'; import * as viewModule from 'angular2/src/core/compiler/view'; import {ViewContainer} from 'angular2/src/core/compiler/view_container'; import {NgElement} from 'angular2/src/core/compiler/ng_element'; import {Directive, Component, onChange, onDestroy, onAllChangesDone} from 'angular2/src/core/annotations/annotations'; import {ChangeDetector, ChangeDetectorRef} from 'angular2/change_detection'; import {QueryList} from './query_list'; +import {reflector} from 'angular2/src/reflection/reflection'; var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10; @@ -194,17 +195,14 @@ export class TreeNode { export class DirectiveDependency extends Dependency { depth:int; - eventEmitterName:string; propSetterName:string; attributeName:string; queryDirective; - constructor(key:Key, asPromise:boolean, lazy:boolean, optional:boolean, - properties:List, depth:int, eventEmitterName: string, - propSetterName: string, attributeName:string, queryDirective) { + constructor(key:Key, asPromise:boolean, lazy:boolean, optional:boolean, properties:List, + depth:int, propSetterName: string, attributeName:string, queryDirective) { super(key, asPromise, lazy, optional, properties); this.depth = depth; - this.eventEmitterName = eventEmitterName; this.propSetterName = propSetterName; this.attributeName = attributeName; this.queryDirective = queryDirective; @@ -213,18 +211,16 @@ export class DirectiveDependency extends Dependency { _verify() { var count = 0; - if (isPresent(this.eventEmitterName)) count++; if (isPresent(this.propSetterName)) count++; if (isPresent(this.queryDirective)) count++; if (isPresent(this.attributeName)) count++; if (count > 1) throw new BaseException( - 'A directive injectable can contain only one of the following @EventEmitter, @PropertySetter, @Attribute or @Query.'); + 'A directive injectable can contain only one of the following @PropertySetter, @Attribute or @Query.'); } static createFrom(d:Dependency):Dependency { return new DirectiveDependency(d.key, d.asPromise, d.lazy, d.optional, d.properties, DirectiveDependency._depth(d.properties), - DirectiveDependency._eventEmitterName(d.properties), DirectiveDependency._propSetterName(d.properties), DirectiveDependency._attributeName(d.properties), DirectiveDependency._query(d.properties) @@ -238,11 +234,6 @@ export class DirectiveDependency extends Dependency { return 0; } - static _eventEmitterName(properties):string { - var p = ListWrapper.find(properties, (p) => p instanceof EventEmitter); - return isPresent(p) ? p.eventName : null; - } - static _propSetterName(properties):string { var p = ListWrapper.find(properties, (p) => p instanceof PropertySetter); return isPresent(p) ? p.propName : null; @@ -277,6 +268,10 @@ export class DirectiveBinding extends ResolvedBinding { } } + get eventEmitters():List { + return isPresent(this.annotation) && isPresent(this.annotation.events) ? this.annotation.events : []; + } + get changeDetection() { if (this.annotation instanceof Component) { var c:Component = this.annotation; @@ -296,10 +291,6 @@ export class DirectiveBinding extends ResolvedBinding { var binding = new Binding(type, {toClass: type}); return DirectiveBinding.createFromBinding(binding, annotation); } - - static _hasEventEmitter(eventName: string, binding: DirectiveBinding) { - return ListWrapper.any(binding.dependencies, (d) => (d.eventEmitterName == eventName)); - } } // TODO(rado): benchmark and consider rolling in as ElementInjector fields. @@ -317,6 +308,16 @@ export class PreBuiltObjects { } } +class EventEmitterAccessor { + eventName:string; + getter:Function; + + constructor(eventName:string, getter:Function) { + this.eventName = eventName; + this.getter = getter; + } +} + /** Difference between di.Injector and ElementInjector @@ -337,17 +338,18 @@ ElementInjector: PERF BENCHMARK: http://www.williambrownstreet.net/blog/2014/04/faster-angularjs-rendering-angularjs-and-reactjs/ */ + export class ProtoElementInjector { - _binding0:ResolvedBinding; - _binding1:ResolvedBinding; - _binding2:ResolvedBinding; - _binding3:ResolvedBinding; - _binding4:ResolvedBinding; - _binding5:ResolvedBinding; - _binding6:ResolvedBinding; - _binding7:ResolvedBinding; - _binding8:ResolvedBinding; - _binding9:ResolvedBinding; + _binding0:DirectiveBinding; + _binding1:DirectiveBinding; + _binding2:DirectiveBinding; + _binding3:DirectiveBinding; + _binding4:DirectiveBinding; + _binding5:DirectiveBinding; + _binding6:DirectiveBinding; + _binding7:DirectiveBinding; + _binding8:DirectiveBinding; + _binding9:DirectiveBinding; _binding0IsComponent:boolean; _keyId0:int; _keyId1:int; @@ -364,6 +366,7 @@ export class ProtoElementInjector { view:viewModule.AppView; distanceToParent:number; attributes:Map; + eventEmitterAccessors:List>; numberOfDirectives:number; @@ -397,22 +400,69 @@ export class ProtoElementInjector { this.numberOfDirectives = bindings.length; var length = bindings.length; + this.eventEmitterAccessors = ListWrapper.createFixedSize(length); - if (length > 0) {this._binding0 = this._createBinding(bindings[0]); this._keyId0 = this._binding0.key.id;} - if (length > 1) {this._binding1 = this._createBinding(bindings[1]); this._keyId1 = this._binding1.key.id;} - if (length > 2) {this._binding2 = this._createBinding(bindings[2]); this._keyId2 = this._binding2.key.id;} - if (length > 3) {this._binding3 = this._createBinding(bindings[3]); this._keyId3 = this._binding3.key.id;} - if (length > 4) {this._binding4 = this._createBinding(bindings[4]); this._keyId4 = this._binding4.key.id;} - if (length > 5) {this._binding5 = this._createBinding(bindings[5]); this._keyId5 = this._binding5.key.id;} - if (length > 6) {this._binding6 = this._createBinding(bindings[6]); this._keyId6 = this._binding6.key.id;} - if (length > 7) {this._binding7 = this._createBinding(bindings[7]); this._keyId7 = this._binding7.key.id;} - if (length > 8) {this._binding8 = this._createBinding(bindings[8]); this._keyId8 = this._binding8.key.id;} - if (length > 9) {this._binding9 = this._createBinding(bindings[9]); this._keyId9 = this._binding9.key.id;} + if (length > 0) { + this._binding0 = this._createBinding(bindings[0]); + this._keyId0 = this._binding0.key.id; + this.eventEmitterAccessors[0] = this._createEventEmitterAccessors(this._binding0); + } + if (length > 1) { + this._binding1 = this._createBinding(bindings[1]); + this._keyId1 = this._binding1.key.id; + this.eventEmitterAccessors[1] = this._createEventEmitterAccessors(this._binding1); + } + if (length > 2) { + this._binding2 = this._createBinding(bindings[2]); + this._keyId2 = this._binding2.key.id; + this.eventEmitterAccessors[2] = this._createEventEmitterAccessors(this._binding2); + } + if (length > 3) { + this._binding3 = this._createBinding(bindings[3]); + this._keyId3 = this._binding3.key.id; + this.eventEmitterAccessors[3] = this._createEventEmitterAccessors(this._binding3); + } + if (length > 4) { + this._binding4 = this._createBinding(bindings[4]); + this._keyId4 = this._binding4.key.id; + this.eventEmitterAccessors[4] = this._createEventEmitterAccessors(this._binding4); + } + if (length > 5) { + this._binding5 = this._createBinding(bindings[5]); + this._keyId5 = this._binding5.key.id; + this.eventEmitterAccessors[5] = this._createEventEmitterAccessors(this._binding5); + } + if (length > 6) { + this._binding6 = this._createBinding(bindings[6]); + this._keyId6 = this._binding6.key.id; + this.eventEmitterAccessors[6] = this._createEventEmitterAccessors(this._binding6); + } + if (length > 7) { + this._binding7 = this._createBinding(bindings[7]); + this._keyId7 = this._binding7.key.id; + this.eventEmitterAccessors[7] = this._createEventEmitterAccessors(this._binding7); + } + if (length > 8) { + this._binding8 = this._createBinding(bindings[8]); + this._keyId8 = this._binding8.key.id; + this.eventEmitterAccessors[8] = this._createEventEmitterAccessors(this._binding8); + } + if (length > 9) { + this._binding9 = this._createBinding(bindings[9]); + this._keyId9 = this._binding9.key.id; + this.eventEmitterAccessors[9] = this._createEventEmitterAccessors(this._binding9); + } if (length > 10) { throw 'Maximum number of directives per element has been reached.'; } } + _createEventEmitterAccessors(b:DirectiveBinding) { + return ListWrapper.map(b.eventEmitters, eventName => + new EventEmitterAccessor(eventName, reflector.getter(eventName)) + ); + } + instantiate(parent:ElementInjector):ElementInjector { return new ElementInjector(this, parent); } @@ -447,21 +497,6 @@ export class ProtoElementInjector { if (index == 9) return this._binding9; throw new OutOfBoundsAccess(index); } - - hasEventEmitter(eventName: string) { - var p = this; - if (isPresent(p._binding0) && DirectiveBinding._hasEventEmitter(eventName, p._binding0)) return true; - if (isPresent(p._binding1) && DirectiveBinding._hasEventEmitter(eventName, p._binding1)) return true; - if (isPresent(p._binding2) && DirectiveBinding._hasEventEmitter(eventName, p._binding2)) return true; - if (isPresent(p._binding3) && DirectiveBinding._hasEventEmitter(eventName, p._binding3)) return true; - if (isPresent(p._binding4) && DirectiveBinding._hasEventEmitter(eventName, p._binding4)) return true; - if (isPresent(p._binding5) && DirectiveBinding._hasEventEmitter(eventName, p._binding5)) return true; - if (isPresent(p._binding6) && DirectiveBinding._hasEventEmitter(eventName, p._binding6)) return true; - if (isPresent(p._binding7) && DirectiveBinding._hasEventEmitter(eventName, p._binding7)) return true; - if (isPresent(p._binding8) && DirectiveBinding._hasEventEmitter(eventName, p._binding8)) return true; - if (isPresent(p._binding9) && DirectiveBinding._hasEventEmitter(eventName, p._binding9)) return true; - return false; - } } export class ElementInjector extends TreeNode { @@ -607,6 +642,10 @@ export class ElementInjector extends TreeNode { return this._getDirectiveByKeyId(Key.get(type).id) !== _undefined; } + getEventEmitterAccessors() { + return this._proto.eventEmitterAccessors; + } + /** Gets the NgElement associated with this ElementInjector */ getNgElement() { return this._preBuiltObjects.element; @@ -689,7 +728,6 @@ export class ElementInjector extends TreeNode { } _getByDependency(dep:DirectiveDependency, requestor:Key) { - if (isPresent(dep.eventEmitterName)) return this._buildEventEmitter(dep); if (isPresent(dep.propSetterName)) return this._buildPropSetter(dep); if (isPresent(dep.attributeName)) return this._buildAttribute(dep); if (isPresent(dep.queryDirective)) return this._findQuery(dep.queryDirective).list; @@ -699,13 +737,6 @@ export class ElementInjector extends TreeNode { return this._getByKey(dep.key, dep.depth, dep.optional, requestor); } - _buildEventEmitter(dep: DirectiveDependency) { - var view = this._getPreBuiltObjectByKeyId(StaticKeys.instance().viewId); - return (event) => { - view.triggerEventHandlers(dep.eventEmitterName, event, this._proto.index); - }; - } - _buildPropSetter(dep) { var view = this._getPreBuiltObjectByKeyId(StaticKeys.instance().viewId); var renderer = view.renderer; @@ -934,18 +965,10 @@ export class ElementInjector extends TreeNode { throw new OutOfBoundsAccess(index); } - getDirectiveBindingAtIndex(index:int) { - return this._proto.getDirectiveBindingAtIndex(index); - } - hasInstances() { return this._constructionCounter > 0; } - hasEventEmitter(eventName: string) { - return this._proto.hasEventEmitter(eventName); - } - /** Gets whether this element is exporting a component instance as $implicit. */ isExportingComponent() { return this._proto.exportComponent; diff --git a/modules/angular2/src/core/compiler/view_hydrator.js b/modules/angular2/src/core/compiler/view_hydrator.js index 86a0c4d787..326ea0313e 100644 --- a/modules/angular2/src/core/compiler/view_hydrator.js +++ b/modules/angular2/src/core/compiler/view_hydrator.js @@ -2,6 +2,7 @@ import {Injectable, Inject, OpaqueToken, Injector} from 'angular2/di'; import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection'; import * as eli from './element_injector'; import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang'; +import {ObservableWrapper} from 'angular2/src/facade/async'; import * as vcModule from './view_container'; import * as viewModule from './view'; import {BindingPropagationConfig, Locals} from 'angular2/change_detection'; @@ -165,6 +166,7 @@ export class AppViewHydrator { var elementInjector = view.elementInjectors[i]; if (isPresent(elementInjector)) { elementInjector.instantiateDirectives(appInjector, hostElementInjector, shadowDomAppInjector, view.preBuiltObjects[i]); + this._setUpEventEmitters(view, elementInjector); // The exporting of $implicit is a special case. Since multiple elements will all export // the different values as $implicit, directly assign $implicit bindings to the variable @@ -194,6 +196,25 @@ export class AppViewHydrator { return renderComponentIndex; } + _setUpEventEmitters(view:viewModule.AppView, elementInjector:eli.ElementInjector) { + var emitters = elementInjector.getEventEmitterAccessors(); + for(var directiveIndex = 0; directiveIndex < emitters.length; ++directiveIndex) { + var directiveEmitters = emitters[directiveIndex]; + var directive = elementInjector.getDirectiveAtIndex(directiveIndex); + + for (var eventIndex = 0; eventIndex < directiveEmitters.length; ++eventIndex) { + var eventEmitterAccessor = directiveEmitters[eventIndex]; + this._setUpSubscription(view, directive, directiveIndex, eventEmitterAccessor); + } + } + } + + _setUpSubscription(view:viewModule.AppView, directive:Object, directiveIndex:number, eventEmitterAccessor) { + var eventEmitter = eventEmitterAccessor.getter(directive); + ObservableWrapper.subscribe(eventEmitter, + eventObj => view.triggerEventHandlers(eventEmitterAccessor.eventName, eventObj, directiveIndex)); + } + /** * This should only be called by View or ViewContainer. */ diff --git a/modules/angular2/src/facade/async.dart b/modules/angular2/src/facade/async.dart index e3b500123a..a4aa5180d8 100644 --- a/modules/angular2/src/facade/async.dart +++ b/modules/angular2/src/facade/async.dart @@ -37,27 +37,50 @@ class ObservableWrapper { return s.listen(onNext, onError: onError, onDone: onComplete, cancelOnError: true); } - static StreamController createController() { - return new StreamController.broadcast(); + static void callNext(EventEmitter emitter, value) { + emitter.add(value); } - static Stream createObservable(StreamController controller) { - return controller.stream; + static void callThrow(EventEmitter emitter, error) { + emitter.addError(error); } - static void callNext(StreamController controller, value) { - controller.add(value); - } - - static void callThrow(StreamController controller, error) { - controller.addError(error); - } - - static void callReturn(StreamController controller) { - controller.close(); + static void callReturn(EventEmitter emitter) { + emitter.close(); } } +class EventEmitter extends Stream { + StreamController _controller; + + EventEmitter() { + _controller = new StreamController.broadcast(); + } + + StreamSubscription listen(void onData(String line), { + void onError(Error error), + void onDone(), + bool cancelOnError }) { + return _controller.stream.listen(onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError); + } + + void add(value) { + _controller.add(value); + } + + void addError(error) { + _controller.addError(error); + } + + void close() { + _controller.close(); + } +} + + class _Completer { final Completer c; diff --git a/modules/angular2/src/facade/async.es6 b/modules/angular2/src/facade/async.es6 index 5e2ca3639c..e9eff1b461 100644 --- a/modules/angular2/src/facade/async.es6 +++ b/modules/angular2/src/facade/async.es6 @@ -53,6 +53,28 @@ export class PromiseWrapper { } } +export class ObservableWrapper { + static subscribe(emitter:EventEmitter, onNext, onThrow = null, onReturn = null) { + return emitter.observer({next: onNext, throw: onThrow, return: onReturn}); + } + + static callNext(emitter:EventEmitter, value:any) { + emitter.next(value); + } + + static callThrow(emitter:EventEmitter, error:any) { + emitter.throw(error); + } + + static callReturn(emitter:EventEmitter) { + emitter.return(); + } +} + +//TODO: vsavkin change to interface +export class Observable { + observer(generator:Function){} +} /** * Use Rx.Observable but provides an adapter to make it work as specified here: @@ -60,39 +82,37 @@ export class PromiseWrapper { * * Once a reference implementation of the spec is available, switch to it. */ -export var Observable = Rx.Observable; -export var ObservableController = Rx.Subject; +export class EventEmitter extends Observable { + _subject:Rx.Subject; -export class ObservableWrapper { - static createController():Rx.Subject { - return new Rx.Subject(); + constructor() { + super(); + this._subject = new Rx.Subject(); } - static createObservable(subject:Rx.Subject):Observable { - return subject; + observer(generator) { + // Rx.Scheduler.immediate and setTimeout is a workaround, so Rx works with zones.js. + // Once https://github.com/angular/zone.js/issues/51 is fixed, the hack should be removed. + return this._subject.observeOn(Rx.Scheduler.immediate).subscribe( + (value) => {setTimeout(() => generator.next(value));}, + (error) => generator.throw ? generator.throw(error) : null, + () => generator.return ? generator.return() : null + ); } - static subscribe(observable:Observable, generatorOrOnNext, onThrow = null, onReturn = null) { - if (isPresent(generatorOrOnNext.next)) { - return observable.observeOn(Rx.Scheduler.timeout).subscribe( - (value) => generatorOrOnNext.next(value), - (error) => generatorOrOnNext.throw(error), - () => generatorOrOnNext.return() - ); - } else { - return observable.observeOn(Rx.Scheduler.timeout).subscribe(generatorOrOnNext, onThrow, onReturn); - } + toRx():Rx.Observable { + return this._subject; } - static callNext(subject:Rx.Subject, value:any) { - subject.onNext(value); + next(value) { + this._subject.onNext(value); } - static callThrow(subject:Rx.Subject, error:any) { - subject.onError(error); + throw(error) { + this._subject.onError(error); } - static callReturn(subject:Rx.Subject) { - subject.onCompleted(); + return(value) { + this._subject.onCompleted(); } } \ No newline at end of file diff --git a/modules/angular2/src/facade/async.ts b/modules/angular2/src/facade/async.ts index c7bdbf9797..17b5f60eaa 100644 --- a/modules/angular2/src/facade/async.ts +++ b/modules/angular2/src/facade/async.ts @@ -49,35 +49,50 @@ export class PromiseWrapper { } +export class ObservableWrapper { + static subscribe(emitter: EventEmitter, onNext, onThrow = null, onReturn = null) { + return emitter.observer({next: onNext, throw: onThrow, return: onReturn}); + } + + static callNext(emitter: EventEmitter, value: any) { emitter.next(value); } + + static callThrow(emitter: EventEmitter, error: any) { emitter.throw(error); } + + static callReturn(emitter: EventEmitter) { emitter.return (null); } +} + +// TODO: vsavkin change to interface +export class Observable { + observer(generator: any) {} +} + /** * Use Rx.Observable but provides an adapter to make it work as specified here: * https://github.com/jhusain/observable-spec * * Once a reference implementation of the spec is available, switch to it. */ -type Observable = Rx.Observable; -type ObservableController = Rx.Subject; +export class EventEmitter extends Observable { + _subject: Rx.Subject; -export class ObservableWrapper { - static createController(): Rx.Subject { return new Rx.Subject(); } - - static createObservable(subject: Rx.Subject): Rx.Observable { return subject; } - - static subscribe(observable: Rx.Observable, generatorOrOnNext, onThrow = null, - onReturn = null) { - if (isPresent(generatorOrOnNext.next)) { - return observable.observeOn(Rx.Scheduler.timeout) - .subscribe((value) => generatorOrOnNext.next(value), - (error) => generatorOrOnNext.throw(error), () => generatorOrOnNext.return ()); - } else { - return observable.observeOn(Rx.Scheduler.timeout) - .subscribe(generatorOrOnNext, onThrow, onReturn); - } + constructor() { + super(); + this._subject = new Rx.Subject(); } - static callNext(subject: Rx.Subject, value: any) { subject.onNext(value); } + observer(generator) { + var immediateScheduler = (Rx.Scheduler).immediate; + return this._subject.observeOn(immediateScheduler) + .subscribe((value) => { setTimeout(() => generator.next(value)); }, + (error) => generator.throw ? generator.throw(error) : null, + () => generator.return ? generator.return () : null); + } - static callThrow(subject: Rx.Subject, error: any) { subject.onError(error); } + toRx(): Rx.Observable { return this._subject; } - static callReturn(subject: Rx.Subject) { subject.onCompleted(); } -} + next(value) { this._subject.onNext(value); } + + throw(error) { this._subject.onError(error); } + + return (value) { this._subject.onCompleted(); } +} \ No newline at end of file diff --git a/modules/angular2/src/forms/model.js b/modules/angular2/src/forms/model.js index cece2670ee..fb1a661a0d 100644 --- a/modules/angular2/src/forms/model.js +++ b/modules/angular2/src/forms/model.js @@ -1,5 +1,5 @@ import {isPresent} from 'angular2/src/facade/lang'; -import {Observable, ObservableController, ObservableWrapper} from 'angular2/src/facade/async'; +import {Observable, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {StringMap, StringMapWrapper, ListWrapper, List} from 'angular2/src/facade/collection'; import {Validators} from './validators'; @@ -40,8 +40,7 @@ export class AbstractControl { _parent:any; /* ControlGroup | ControlArray */ validator:Function; - valueChanges:Observable; - _valueChangesController:ObservableController; + _valueChanges:EventEmitter; constructor(validator:Function) { this.validator = validator; @@ -72,6 +71,10 @@ export class AbstractControl { return ! this.pristine; } + get valueChanges():Observable { + return this._valueChanges; + } + setParent(parent){ this._parent = parent; } @@ -95,16 +98,14 @@ export class Control extends AbstractControl { constructor(value:any, validator:Function = Validators.nullValidator) { super(validator); this._setValueErrorsStatus(value); - - this._valueChangesController = ObservableWrapper.createController(); - this.valueChanges = ObservableWrapper.createObservable(this._valueChangesController); + this._valueChanges = new EventEmitter(); } updateValue(value:any):void { this._setValueErrorsStatus(value); this._pristine = false; - ObservableWrapper.callNext(this._valueChangesController, this._value); + ObservableWrapper.callNext(this._valueChanges, this._value); this._updateParent(); } @@ -137,8 +138,7 @@ export class ControlGroup extends AbstractControl { this.controls = controls; this._optionals = isPresent(optionals) ? optionals : {}; - this._valueChangesController = ObservableWrapper.createController(); - this.valueChanges = ObservableWrapper.createObservable(this._valueChangesController); + this._valueChanges = new EventEmitter(); this._setParentForControls(); this._setValueErrorsStatus(); @@ -169,7 +169,7 @@ export class ControlGroup extends AbstractControl { this._setValueErrorsStatus(); this._pristine = false; - ObservableWrapper.callNext(this._valueChangesController, this._value); + ObservableWrapper.callNext(this._valueChanges, this._value); this._updateParent(); } @@ -222,8 +222,7 @@ export class ControlArray extends AbstractControl { super(validator); this.controls = controls; - this._valueChangesController = ObservableWrapper.createController(); - this.valueChanges = ObservableWrapper.createObservable(this._valueChangesController); + this._valueChanges = new EventEmitter(); this._setParentForControls(); this._setValueErrorsStatus(); @@ -258,7 +257,7 @@ export class ControlArray extends AbstractControl { this._setValueErrorsStatus(); this._pristine = false; - ObservableWrapper.callNext(this._valueChangesController, this._value); + ObservableWrapper.callNext(this._valueChanges, this._value); this._updateParent(); } diff --git a/modules/angular2/test/core/compiler/element_injector_spec.js b/modules/angular2/test/core/compiler/element_injector_spec.js index a402799bb4..4183cee3a8 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.js +++ b/modules/angular2/test/core/compiler/element_injector_spec.js @@ -4,7 +4,7 @@ import {ListWrapper, MapWrapper, List, StringMapWrapper, iterateListLike} from ' import {ProtoElementInjector, PreBuiltObjects, DirectiveBinding, TreeNode, ElementRef} from 'angular2/src/core/compiler/element_injector'; import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility'; -import {EventEmitter, PropertySetter, Attribute, Query} from 'angular2/src/core/annotations/di'; +import {PropertySetter, Attribute, Query} from 'angular2/src/core/annotations/di'; import {onDestroy} from 'angular2/src/core/annotations/annotations'; import {Optional, Injector, Inject, bind} from 'angular2/di'; import {AppProtoView, AppView} from 'angular2/src/core/compiler/view'; @@ -12,11 +12,11 @@ import {ViewContainer} from 'angular2/src/core/compiler/view_container'; import {NgElement} from 'angular2/src/core/compiler/ng_element'; import {Directive} from 'angular2/src/core/annotations/annotations'; import {DynamicChangeDetector, ChangeDetectorRef, Parser, Lexer} from 'angular2/change_detection'; -import {ViewRef, Renderer, EventBinding} from 'angular2/src/render/api'; +import {ViewRef, Renderer} from 'angular2/src/render/api'; import {QueryList} from 'angular2/src/core/compiler/query_list'; class DummyDirective extends Directive { - constructor({lifecycle} = {}) { super({lifecycle: lifecycle}); } + constructor({lifecycle, events} = {}) { super({lifecycle: lifecycle, events: events}); } } @proxy @@ -80,23 +80,10 @@ class NeedsService { } } -class NeedsEventEmitter { - clickEmitter; - constructor(@EventEmitter('click') clickEmitter:Function) { - this.clickEmitter = clickEmitter; - } - click() { - this.clickEmitter(null); - } -} - -class NeedsEventEmitterNoType { - clickEmitter; - constructor(@EventEmitter('click') clickEmitter) { - this.clickEmitter = clickEmitter; - } - click() { - this.clickEmitter(null); +class HasEventEmitter { + emitter; + constructor() { + this.emitter = "emitter"; } } @@ -380,6 +367,20 @@ export function main() { 'Index 10 is out-of-bounds.'); }); }); + + describe('event emitters', () => { + it('should return a list of event emitter accessors', () => { + var binding = DirectiveBinding.createFromType( + HasEventEmitter, new DummyDirective({events: ['emitter']})); + + var inj = new ProtoElementInjector(null, 0, [binding]); + expect(inj.eventEmitterAccessors.length).toEqual(1); + + var accessor = inj.eventEmitterAccessors[0][0]; + expect(accessor.eventName).toEqual('emitter'); + expect(accessor.getter(new HasEventEmitter())).toEqual('emitter'); + }); + }); }); describe("ElementInjector", function () { @@ -703,51 +704,6 @@ export function main() { }); }); - describe('event emitters', () => { - - function createpreBuildObject(eventName, eventHandler) { - var handlers = StringMapWrapper.create(); - StringMapWrapper.set(handlers, eventName, eventHandler); - var pv = new AppProtoView(null, null); - pv.bindElement(null, 0, null, null, null); - var eventBindings = ListWrapper.create(); - ListWrapper.push(eventBindings, new EventBinding(eventName, new Parser(new Lexer()).parseAction('handler()', ''))); - pv.bindEvent(eventBindings); - - var view = new AppView(null, pv, MapWrapper.create()); - view.context = new ContextWithHandler(eventHandler); - return new PreBuiltObjects(view, null, null, null); - } - - it('should be injectable and callable', () => { - var called = false; - var preBuildObject = createpreBuildObject('click', () => { called = true;}); - var inj = injector([NeedsEventEmitter], null, null, preBuildObject); - inj.get(NeedsEventEmitter).click(); - expect(called).toEqual(true); - }); - - it('should be injectable and callable without specifying param type annotation', () => { - var called = false; - var preBuildObject = createpreBuildObject('click', () => { called = true;}); - var inj = injector([NeedsEventEmitterNoType], null, null, preBuildObject); - inj.get(NeedsEventEmitterNoType).click(); - expect(called).toEqual(true); - }); - - it('should be queryable through hasEventEmitter', () => { - var inj = injector([NeedsEventEmitter]); - expect(inj.hasEventEmitter('click')).toBe(true); - expect(inj.hasEventEmitter('move')).toBe(false); - }); - - it('should be queryable through hasEventEmitter without specifying param type annotation', () => { - var inj = injector([NeedsEventEmitterNoType]); - expect(inj.hasEventEmitter('click')).toBe(true); - expect(inj.hasEventEmitter('move')).toBe(false); - }); - }); - describe('property setter', () => { var renderer, view; diff --git a/modules/angular2/test/core/compiler/integration_spec.js b/modules/angular2/test/core/compiler/integration_spec.js index 8487e96c75..189b260b85 100644 --- a/modules/angular2/test/core/compiler/integration_spec.js +++ b/modules/angular2/test/core/compiler/integration_spec.js @@ -18,7 +18,7 @@ import {TestBed} from 'angular2/src/test_lib/test_bed'; import {DOM} from 'angular2/src/dom/dom_adapter'; import {Type, isPresent, BaseException, assertionsEnabled, isJsObject, global} from 'angular2/src/facade/lang'; -import {PromiseWrapper} from 'angular2/src/facade/async'; +import {PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {Injector, bind} from 'angular2/di'; import {dynamicChangeDetection, @@ -27,7 +27,7 @@ import {dynamicChangeDetection, import {Decorator, Component, Viewport, DynamicComponent} from 'angular2/src/core/annotations/annotations'; import {View} from 'angular2/src/core/annotations/view'; import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility'; -import {EventEmitter, Attribute} from 'angular2/src/core/annotations/di'; +import {Attribute} from 'angular2/src/core/annotations/di'; import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader'; import {ElementRef} from 'angular2/src/core/compiler/element_injector'; @@ -532,14 +532,14 @@ export function main() { var emitter = injector.get(DecoratorEmitingEvent); var listener = injector.get(DecoratorListeningEvent); - expect(emitter.msg).toEqual(''); expect(listener.msg).toEqual(''); emitter.fireEvent('fired !'); - expect(emitter.msg).toEqual('fired !'); - expect(listener.msg).toEqual('fired !'); - async.done(); + PromiseWrapper.setTimeout(() => { + expect(listener.msg).toEqual('fired !'); + async.done(); + }, 0); }); })); @@ -994,23 +994,19 @@ class DoublePipeFactory { @Decorator({ selector: '[emitter]', - hostListeners: {'event': 'onEvent($event)'} + events: ['event'] }) class DecoratorEmitingEvent { msg: string; - emitter; + event:EventEmitter; - constructor(@EventEmitter('event') emitter:Function) { + constructor() { this.msg = ''; - this.emitter = emitter; + this.event = new EventEmitter(); } fireEvent(msg: string) { - this.emitter(msg); - } - - onEvent(msg: string) { - this.msg = msg; + ObservableWrapper.callNext(this.event, msg); } } diff --git a/modules/angular2/test/core/compiler/view_hydrator_spec.js b/modules/angular2/test/core/compiler/view_hydrator_spec.js index 995f10644d..223e01505d 100644 --- a/modules/angular2/test/core/compiler/view_hydrator_spec.js +++ b/modules/angular2/test/core/compiler/view_hydrator_spec.js @@ -47,6 +47,7 @@ export function main() { var res = new SpyElementInjector(); res.spy('isExportingComponent').andCallFake( () => false ); res.spy('isExportingElement').andCallFake( () => false ); + res.spy('getEventEmitterAccessors').andCallFake( () => [] ); return res; } diff --git a/modules/angular2/test/facade/async_spec.js b/modules/angular2/test/facade/async_spec.js index 825fb39ab6..f1af69cd6e 100644 --- a/modules/angular2/test/facade/async_spec.js +++ b/modules/angular2/test/facade/async_spec.js @@ -1,99 +1,61 @@ import {describe, it, expect, beforeEach, ddescribe, iit, xit, el, SpyObject, AsyncTestCompleter, inject, IS_DARTIUM} from 'angular2/test_lib'; -import {ObservableWrapper, Observable, ObservableController, PromiseWrapper} from 'angular2/src/facade/async'; +import {ObservableWrapper, EventEmitter, PromiseWrapper} from 'angular2/src/facade/async'; export function main() { - describe('Observable', () => { - var obs:Observable; - var controller:ObservableController; - + describe('EventEmitter', () => { + var emitter:EventEmitter; + beforeEach(() => { - controller = ObservableWrapper.createController(); - obs = ObservableWrapper.createObservable(controller); + emitter = new EventEmitter(); }); it("should call the next callback", inject([AsyncTestCompleter], (async) => { - ObservableWrapper.subscribe(obs, (value) => { + ObservableWrapper.subscribe(emitter, (value) => { expect(value).toEqual(99); async.done(); }); - ObservableWrapper.callNext(controller, 99); + ObservableWrapper.callNext(emitter, 99); })); it("should call the throw callback", inject([AsyncTestCompleter], (async) => { - ObservableWrapper.subscribe(obs, (_) => {}, (error) => { + ObservableWrapper.subscribe(emitter, (_) => {}, (error) => { expect(error).toEqual("Boom"); async.done(); }); - ObservableWrapper.callThrow(controller, "Boom"); + ObservableWrapper.callThrow(emitter, "Boom"); + })); + + it("should work when no throw callback is provided", inject([AsyncTestCompleter], (async) => { + ObservableWrapper.subscribe(emitter, (_) => {}, (_) => { + async.done(); + }); + ObservableWrapper.callThrow(emitter, "Boom"); })); it("should call the return callback", inject([AsyncTestCompleter], (async) => { - ObservableWrapper.subscribe(obs, (_) => {}, (_) => {}, () => { + ObservableWrapper.subscribe(emitter, (_) => {}, (_) => {}, () => { async.done(); }); - ObservableWrapper.callReturn(controller); + ObservableWrapper.callReturn(emitter); })); it("should subscribe to the wrapper asynchronously", () => { var called = false; - ObservableWrapper.subscribe(obs, (value) => { + ObservableWrapper.subscribe(emitter, (value) => { called = true; }); - ObservableWrapper.callNext(controller, 99); + ObservableWrapper.callNext(emitter, 99); expect(called).toBe(false); }); - if (!IS_DARTIUM) { - // See here: https://github.com/jhusain/observable-spec - describe("Generator", () => { - var generator; - - beforeEach(() => { - generator = new SpyObject(); - generator.spy("next"); - generator.spy("throw"); - generator.spy("return"); - }); - - it("should call next on the given generator", inject([AsyncTestCompleter], (async) => { - generator.spy("next").andCallFake((value) => { - expect(value).toEqual(99); - async.done(); - }); - - ObservableWrapper.subscribe(obs, generator); - ObservableWrapper.callNext(controller, 99); - })); - - it("should call throw on the given generator", inject([AsyncTestCompleter], (async) => { - generator.spy("throw").andCallFake((error) => { - expect(error).toEqual("Boom"); - async.done(); - }); - ObservableWrapper.subscribe(obs, generator); - ObservableWrapper.callThrow(controller, "Boom"); - })); - - it("should call return on the given generator", inject([AsyncTestCompleter], (async) => { - generator.spy("return").andCallFake(() => { - async.done(); - }); - ObservableWrapper.subscribe(obs, generator); - ObservableWrapper.callReturn(controller); - })); - }); - } - //TODO: vsavkin: add tests cases //should call dispose on the subscription if generator returns {done:true} //should call dispose on the subscription on throw //should call dispose on the subscription on return }); -} - -//make sure rx observables are async \ No newline at end of file +} \ No newline at end of file diff --git a/modules/angular2_material/src/components/radio/radio_button.js b/modules/angular2_material/src/components/radio/radio_button.js index 00e145fc92..379cd1f32e 100644 --- a/modules/angular2_material/src/components/radio/radio_button.js +++ b/modules/angular2_material/src/components/radio/radio_button.js @@ -1,11 +1,10 @@ -import {Component, View, Parent, Ancestor, Attribute, PropertySetter, - EventEmitter} from 'angular2/angular2'; +import {Component, View, Parent, Ancestor, Attribute, PropertySetter} from 'angular2/angular2'; import {Optional} from 'angular2/src/di/annotations'; import {MdRadioDispatcher} from 'angular2_material/src/components/radio/radio_dispatcher' import {MdTheme} from 'angular2_material/src/core/theme' import {onChange} from 'angular2/src/core/annotations/annotations'; import {isPresent, StringWrapper} from 'angular2/src/facade/lang'; -// import {KeyCodes} from 'angular2_material/src/core/constants' +import {ObservableWrapper, EventEmitter} from 'angular2/src/facade/async'; import {Math} from 'angular2/src/facade/math'; import {ListWrapper} from 'angular2/src/facade/collection'; @@ -177,6 +176,7 @@ export class MdRadioButton { @Component({ selector: 'md-radio-group', lifecycle: [onChange], + events: ['change'], properties: { 'disabled': 'disabled', 'value': 'value' @@ -212,6 +212,8 @@ export class MdRadioGroup { /** The ID of the selected radio button. */ selectedRadioId: string; + change:EventEmitter; + constructor( @Attribute('tabindex') tabindex: string, @Attribute('disabled') disabled: string, @@ -219,11 +221,10 @@ export class MdRadioGroup { @PropertySetter('attr.role') roleSetter: Function, @PropertySetter('attr.aria-disabled') ariaDisabledSetter: Function, @PropertySetter('attr.aria-activedescendant') ariaActiveDescendantSetter: Function, - @EventEmitter('change') changeEmitter: Function, radioDispatcher: MdRadioDispatcher) { this.name_ = `md-radio-group-${_uniqueIdCounter++}`; this.radios_ = []; - this.changeEmitter = changeEmitter; + this.change = new EventEmitter(); this.ariaActiveDescendantSetter = ariaActiveDescendantSetter; this.ariaDisabledSetter = ariaDisabledSetter; this.radioDispatcher = radioDispatcher; @@ -277,7 +278,7 @@ export class MdRadioGroup { this.value = value; this.selectedRadioId = id; this.ariaActiveDescendantSetter(id); - this.changeEmitter(); + ObservableWrapper.callNext(this.change, null); } /** Registers a child radio button with this group. */ diff --git a/modules/examples/src/forms/index.es6 b/modules/examples/src/forms/index.es6 index e13215625d..119da01a3f 100644 --- a/modules/examples/src/forms/index.es6 +++ b/modules/examples/src/forms/index.es6 @@ -53,6 +53,7 @@ class HeaderFields { // This component is self-contained and can be tested in isolation. @Component({ selector: 'survey-question', + events: ['destroy'], properties: { "question" : "question", "index" : "index" @@ -100,16 +101,16 @@ class HeaderFields { class SurveyQuestion { question:ControlGroup; index:number; - onDelete:Function; + destroy:EventEmitter; - constructor(@EventEmitter("delete") onDelete:Function) { - this.onDelete = onDelete; + constructor() { + this.destroy = new EventEmitter(); } deleteQuestion() { // Invoking an injected event emitter will fire an event, // which in this case will result in calling `deleteQuestion(i)` - this.onDelete(); + this.destroy.next(null); } } @@ -132,7 +133,7 @@ class SurveyQuestion { *for="var q of form.controls.questions.controls; var i=index" [question]="q" [index]="i + 1" - (delete)="deleteQuestion(i)"> + (destroy)="destroyQuestion(i)"> @@ -175,14 +176,14 @@ class SurveyBuilder { // complex form interactions in a declarative fashion. // // We are disabling the responseLength control when the question type is checkbox. - newQuestion.controls.type.valueChanges.subscribe((v) => - v == 'text' || v == 'textarea' ? - newQuestion.include('responseLength') : newQuestion.exclude('responseLength')); + newQuestion.controls.type.valueChanges.observer({ + next: (v) => v == 'text' || v == 'textarea' ? newQuestion.include('responseLength') : newQuestion.exclude('responseLength') + }); this.form.controls.questions.push(newQuestion); } - deleteQuestion(index:number) { + destroyQuestion(index:number) { this.form.controls.questions.removeAt(index); }