From fd34a563471f7b4ed70fa599ebd2acc6587e15ce Mon Sep 17 00:00:00 2001 From: Rado Kirov Date: Fri, 16 Jan 2015 15:30:22 -0800 Subject: [PATCH] feat(events): adds support for injectable angular event emitters. Event emitters can be injected into Directives. Event emitters take over browser events with the same name. Emitted events do not bubble. Event emitters can be injected even if there is no corresponding callback in the template. Use as follows: @Decorator(...) class MyDec(@EventEmitter('click') clickEmitter) { ... fireClick() { var eventData = {...}; this._clickEmitter(eventData); } } --- .../element_injector_benchmark.js | 4 +- modules/core/src/annotations/events.js | 14 ++++ modules/core/src/compiler/element_injector.js | 65 +++++++++++++++++-- modules/core/src/compiler/view.js | 37 ++++++----- .../test/compiler/element_injector_spec.js | 48 ++++++++++---- modules/core/test/compiler/view_spec.js | 43 ++++++++++-- modules/core/test/compiler/viewport_spec.js | 2 +- 7 files changed, 171 insertions(+), 42 deletions(-) create mode 100644 modules/core/src/annotations/events.js diff --git a/modules/benchmarks/src/element_injector/element_injector_benchmark.js b/modules/benchmarks/src/element_injector/element_injector_benchmark.js index 08dffa1d6b..e2f8dc63c2 100644 --- a/modules/benchmarks/src/element_injector/element_injector_benchmark.js +++ b/modules/benchmarks/src/element_injector/element_injector_benchmark.js @@ -31,11 +31,11 @@ export function main() { var bindings = [A, B, C]; var proto = new ProtoElementInjector(null, 0, bindings); - var elementInjector = proto.instantiate(null,null); + var elementInjector = proto.instantiate(null,null, null); function instantiate () { for (var i = 0; i < iterations; ++i) { - var ei = proto.instantiate(null, null); + var ei = proto.instantiate(null, null, null); ei.instantiateDirectives(appInjector, null, null); } } diff --git a/modules/core/src/annotations/events.js b/modules/core/src/annotations/events.js new file mode 100644 index 0000000000..bfe4d08baa --- /dev/null +++ b/modules/core/src/annotations/events.js @@ -0,0 +1,14 @@ +import {CONST} from 'facade/lang'; +import {DependencyAnnotation} from 'di/di'; + +/** + * The directive can inject an emitter function that would emit events onto the + * directive host element. + */ +export class EventEmitter extends DependencyAnnotation { + eventName: string; + @CONST() + constructor(eventName) { + this.eventName = eventName; + } +} diff --git a/modules/core/src/compiler/element_injector.js b/modules/core/src/compiler/element_injector.js index 1b8453329c..b6b3e9b3bc 100644 --- a/modules/core/src/compiler/element_injector.js +++ b/modules/core/src/compiler/element_injector.js @@ -1,10 +1,11 @@ import {FIELD, isPresent, isBlank, Type, int, BaseException} from 'facade/lang'; import {Math} from 'facade/math'; -import {List, ListWrapper} from 'facade/collection'; +import {List, ListWrapper, MapWrapper} from 'facade/collection'; import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError, CyclicDependencyError} from 'di/di'; import {Parent, Ancestor} from 'core/annotations/visibility'; +import {EventEmitter} from 'core/annotations/events'; import {onDestroy} from 'core/annotations/annotations'; -import {View} from 'core/compiler/view'; +import {View, ProtoView} from 'core/compiler/view'; import {LightDom, SourceLightDom, DestinationLightDom} from 'core/compiler/shadow_dom_emulation/light_dom'; import {ViewPort} from 'core/compiler/viewport'; import {NgElement} from 'core/dom/element'; @@ -89,15 +90,18 @@ class TreeNode { export class DirectiveDependency extends Dependency { depth:int; + eventEmitterName:string; - constructor(key:Key, asPromise:boolean, lazy:boolean, properties:List, depth:int) { + constructor(key:Key, asPromise:boolean, lazy:boolean, properties:List, depth:int, eventEmitterName: string) { super(key, asPromise, lazy, properties); this.depth = depth; + this.eventEmitterName = eventEmitterName; } static createFrom(d:Dependency):Dependency { return new DirectiveDependency(d.key, d.asPromise, d.lazy, - d.properties, DirectiveDependency._depth(d.properties)); + d.properties, DirectiveDependency._depth(d.properties), + DirectiveDependency._eventEmitterName(d.properties)); } static _depth(properties):int { @@ -106,6 +110,15 @@ export class DirectiveDependency extends Dependency { if (ListWrapper.any(properties, p => p instanceof Ancestor)) return MAX_DEPTH; return 0; } + + static _eventEmitterName(properties):string { + for (var i = 0; i < properties.length; i++) { + if (properties[i] instanceof EventEmitter) { + return properties[i].eventName; + } + } + return null; + } } export class DirectiveBinding extends Binding { @@ -128,6 +141,10 @@ export class DirectiveBinding extends Binding { var binding = bind(type).toClass(type); return DirectiveBinding.createFromBinding(binding, annotation); } + + static _hasEventEmitter(eventName: string, binding: DirectiveBinding) { + return ListWrapper.any(binding.dependencies, (d) => (d.eventEmitterName == eventName)); + } } @@ -225,8 +242,8 @@ export class ProtoElementInjector { } } - instantiate(parent:ElementInjector, host:ElementInjector):ElementInjector { - return new ElementInjector(this, parent, host); + instantiate(parent:ElementInjector, host:ElementInjector, eventCallbacks):ElementInjector { + return new ElementInjector(this, parent, host, eventCallbacks); } _createBinding(bindingOrType) { @@ -241,6 +258,21 @@ export class ProtoElementInjector { get hasBindings():boolean { return isPresent(this._binding0); } + + 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 { @@ -261,7 +293,8 @@ export class ElementInjector extends TreeNode { _view:View; _preBuiltObjects; _constructionCounter; - constructor(proto:ProtoElementInjector, parent:ElementInjector, host:ElementInjector) { + _eventCallbacks; + constructor(proto:ProtoElementInjector, parent:ElementInjector, host:ElementInjector, eventCallbacks: Map) { super(parent); if (isPresent(parent) && isPresent(host)) { throw new BaseException('Only either parent or host is allowed'); @@ -279,6 +312,7 @@ export class ElementInjector extends TreeNode { this._preBuiltObjects = null; this._lightDomAppInjector = null; this._shadowDomAppInjector = null; + this._eventCallbacks = eventCallbacks; this._obj0 = null; this._obj1 = null; this._obj2 = null; @@ -431,9 +465,22 @@ export class ElementInjector extends TreeNode { } _getByDependency(dep:DirectiveDependency, requestor:Key) { + if (isPresent(dep.eventEmitterName)) return this._buildEventEmitter(dep); return this._getByKey(dep.key, dep.depth, requestor); } + _buildEventEmitter(dep) { + var view = this._getPreBuiltObjectByKeyId(StaticKeys.instance().viewId); + if (isPresent(this._eventCallbacks)) { + var callback = MapWrapper.get(this._eventCallbacks, dep.eventEmitterName); + if (isPresent(callback)) { + var locals = MapWrapper.create(); + return ProtoView.buildInnerCallback(callback, view, locals); + } + } + return (_) => {}; + } + /* * It is fairly easy to annotate keys with metadata. * For example, key.metadata = 'directive'. @@ -534,6 +581,10 @@ export class ElementInjector extends TreeNode { hasInstances() { return this._constructionCounter > 0; } + + hasEventEmitter(eventName: string) { + return this._proto.hasEventEmitter(eventName); + } } class OutOfBoundsAccess extends Error { diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index aa1c541b9e..44192550fa 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -325,9 +325,9 @@ export class ProtoView { if (isPresent(protoElementInjector)) { if (isPresent(protoElementInjector.parent)) { var parentElementInjector = elementInjectors[protoElementInjector.parent.index]; - elementInjector = protoElementInjector.instantiate(parentElementInjector, null); + elementInjector = protoElementInjector.instantiate(parentElementInjector, null, binder.events); } else { - elementInjector = protoElementInjector.instantiate(null, hostElementInjector); + elementInjector = protoElementInjector.instantiate(null, hostElementInjector, binder.events); ListWrapper.push(rootElementInjectors, elementInjector); } } @@ -376,10 +376,10 @@ export class ProtoView { // events if (isPresent(binder.events)) { - // TODO(rado): if there is directive at this element that injected an - // event emitter for that eventType do not attach the handler. MapWrapper.forEach(binder.events, (expr, eventName) => { - ProtoView._addNativeEventListener(element, eventName, expr, view); + if (isBlank(elementInjector) || !elementInjector.hasEventEmitter(eventName)) { + ProtoView._addNativeEventListener(element, eventName, expr, view); + } }); } } @@ -390,23 +390,30 @@ export class ProtoView { return view; } - static _addNativeEventListener(element: Element, eventName: string, expr, view: View) { + static _addNativeEventListener(element: Element, eventName: string, expr: AST, view: View) { var locals = MapWrapper.create(); + var innerCallback = ProtoView.buildInnerCallback(expr, view, locals); DOM.on(element, eventName, (event) => { if (event.target === element) { - // Most of the time the event will be fired only when the view is - // in the live document. However, in a rare circumstance the - // view might get dehydrated, in between the event queuing up and - // firing. - if (view.hydrated()) { - MapWrapper.set(locals, '\$event', event); - var context = new ContextWithVariableBindings(view.context, locals); - expr.eval(context); - } + innerCallback(event); } }); } + static buildInnerCallback(expr:AST, view:View, locals: Map) { + return (event) => { + // Most of the time the event will be fired only when the view is + // in the live document. However, in a rare circumstance the + // view might get dehydrated, in between the event queuing up and + // firing. + if (view.hydrated()) { + MapWrapper.set(locals, '\$event', event); + var context = new ContextWithVariableBindings(view.context, locals); + expr.eval(context); + } + } + } + _parentElementLightDom(protoElementInjector:ProtoElementInjector, preBuiltObjects:List):LightDom { var p = protoElementInjector.parent; return isPresent(p) ? preBuiltObjects[p.index].lightDom : null; diff --git a/modules/core/test/compiler/element_injector_spec.js b/modules/core/test/compiler/element_injector_spec.js index 19daae17c4..61ec3ca170 100644 --- a/modules/core/test/compiler/element_injector_spec.js +++ b/modules/core/test/compiler/element_injector_spec.js @@ -3,6 +3,7 @@ import {isBlank, isPresent, FIELD, IMPLEMENTS, proxy} from 'facade/lang'; import {ListWrapper, MapWrapper, List} from 'facade/collection'; import {ProtoElementInjector, PreBuiltObjects, DirectiveBinding} from 'core/compiler/element_injector'; import {Parent, Ancestor} from 'core/annotations/visibility'; +import {EventEmitter} from 'core/annotations/events'; import {onDestroy} from 'core/annotations/annotations'; import {Injector, Inject, bind} from 'di/di'; import {View} from 'core/compiler/view'; @@ -56,6 +57,16 @@ class NeedsService { } } +class NeedsEventEmitter { + clickEmitter; + constructor(@EventEmitter('click') clickEmitter:Function) { + this.clickEmitter = clickEmitter; + } + click() { + this.clickEmitter(null); + } +} + class A_Needs_B { constructor(dep){} } @@ -100,7 +111,7 @@ export function main() { if (isBlank(lightDomAppInjector)) lightDomAppInjector = new Injector([]); var proto = new ProtoElementInjector(null, 0, bindings, isPresent(shadowDomAppInjector)); - var inj = proto.instantiate(null, null); + var inj = proto.instantiate(null, null, null); var preBuilt = isPresent(preBuiltObjects) ? preBuiltObjects : defaultPreBuiltObjects; inj.instantiateDirectives(lightDomAppInjector, shadowDomAppInjector, preBuilt); @@ -113,12 +124,12 @@ export function main() { var inj = new Injector([]); var protoParent = new ProtoElementInjector(null, 0, parentBindings); - var parent = protoParent.instantiate(null, null); + var parent = protoParent.instantiate(null, null, null); parent.instantiateDirectives(inj, null, parentPreBuildObjects); var protoChild = new ProtoElementInjector(protoParent, 1, childBindings, false, 1); - var child = protoChild.instantiate(parent, null); + var child = protoChild.instantiate(parent, null, null); child.instantiateDirectives(inj, null, defaultPreBuiltObjects); return child; @@ -131,11 +142,11 @@ export function main() { var shadowInj = inj.createChild([]); var protoParent = new ProtoElementInjector(null, 0, hostBindings, true); - var host = protoParent.instantiate(null, null); + var host = protoParent.instantiate(null, null, null); host.instantiateDirectives(inj, shadowInj, hostPreBuildObjects); var protoChild = new ProtoElementInjector(protoParent, 0, shadowBindings, false, 1); - var shadow = protoChild.instantiate(null, host); + var shadow = protoChild.instantiate(null, host, null); shadow.instantiateDirectives(shadowInj, null, null); return shadow; @@ -148,9 +159,9 @@ export function main() { var protoChild1 = new ProtoElementInjector(protoParent, 1, []); var protoChild2 = new ProtoElementInjector(protoParent, 2, []); - var p = protoParent.instantiate(null, null); - var c1 = protoChild1.instantiate(p, null); - var c2 = protoChild2.instantiate(p, null); + var p = protoParent.instantiate(null, null, null); + var c1 = protoChild1.instantiate(p, null, null); + var c2 = protoChild2.instantiate(p, null, null); expect(humanize(p, [ [p, 'parent'], @@ -165,8 +176,8 @@ export function main() { var protoParent = new ProtoElementInjector(null, 0, []); var protoChild = new ProtoElementInjector(protoParent, 1, [], false, distance); - var p = protoParent.instantiate(null, null); - var c = protoChild.instantiate(p, null); + var p = protoParent.instantiate(null, null, null); + var c = protoChild.instantiate(p, null, null); expect(c.directParent()).toEqual(p); }); @@ -176,8 +187,8 @@ export function main() { var protoParent = new ProtoElementInjector(null, 0, []); var protoChild = new ProtoElementInjector(protoParent, 1, [], false, distance); - var p = protoParent.instantiate(null, null); - var c = protoChild.instantiate(p, null); + var p = protoParent.instantiate(null, null, null); + var c = protoChild.instantiate(p, null, null); expect(c.directParent()).toEqual(null); }); @@ -400,5 +411,18 @@ export function main() { }); }); }); + + describe('event emitters', () => { + it('should be injectable and callable', () => { + var inj = injector([NeedsEventEmitter]); + inj.get(NeedsEventEmitter).click(); + }); + + it('should be queryable through hasEventEmitter', () => { + var inj = injector([NeedsEventEmitter]); + expect(inj.hasEventEmitter('click')).toBe(true); + expect(inj.hasEventEmitter('move')).toBe(false); + }); + }); }); } diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index ed4d0007c1..e4bb8737c3 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -7,6 +7,7 @@ import {Component, Decorator, Template} from 'core/annotations/annotations'; import {OnChange} from 'core/core'; import {Lexer, Parser, ProtoChangeDetector, ChangeDetector} from 'change_detection/change_detection'; import {TemplateConfig} from 'core/annotations/template_config'; +import {EventEmitter} from 'core/annotations/events'; import {List, MapWrapper} from 'facade/collection'; import {DOM, Element} from 'facade/dom'; import {int, proxy, IMPLEMENTS} from 'facade/lang'; @@ -233,7 +234,7 @@ export function main() { pv.bindElement(testProtoElementInjector); var hostProtoInjector = new ProtoElementInjector(null, 0, []); - var hostInjector = hostProtoInjector.instantiate(null, null); + var hostInjector = hostProtoInjector.instantiate(null, null, null); var view; expect(() => view = pv.instantiate(hostInjector)).not.toThrow(); expect(testProtoElementInjector.parentElementInjector).toBe(view.elementInjectors[0]); @@ -248,7 +249,7 @@ export function main() { pv.bindElement(testProtoElementInjector); var hostProtoInjector = new ProtoElementInjector(null, 0, []); - var hostInjector = hostProtoInjector.instantiate(null, null); + var hostInjector = hostProtoInjector.instantiate(null, null, null); expect(() => pv.instantiate(hostInjector)).not.toThrow(); expect(testProtoElementInjector.parentElementInjector).toBeNull(); expect(testProtoElementInjector.hostElementInjector).toBe(hostInjector); @@ -453,9 +454,30 @@ export function main() { createViewAndContext(createProtoView()); view.dehydrate(); - dispatchClick(view.nodes[0]); + expect(() => dispatchClick(view.nodes[0])).not.toThrow(); expect(called).toEqual(0); }); + + it('should support custom event emitters', () => { + var pv = new ProtoView(el('
'), + new ProtoChangeDetector()); + pv.bindElement(new TestProtoElementInjector(null, 0, [EventEmitterDirective])); + pv.bindEvent('click', parser.parseBinding('callMe(\$event)', null)); + + createViewAndContext(pv); + var dir = view.elementInjectors[0].get(EventEmitterDirective); + + var dispatchedEvent = new Object(); + + dir.click(dispatchedEvent); + expect(receivedEvent).toBe(dispatchedEvent); + expect(called).toEqual(1); + + // Should not eval the binding, because custom emitter takes over. + dispatchClick(view.nodes[0]); + + expect(called).toEqual(1); + }); }); describe('react to record changes', () => { @@ -634,6 +656,17 @@ class AnotherDirective { } } +class EventEmitterDirective { + _clicker:Function; + constructor(@EventEmitter('click') clicker:Function) { + this._clicker = clicker; + } + click(eventData) { + this._clicker(eventData); + } +} + + class MyEvaluationContext { foo:string; a; @@ -652,9 +685,9 @@ class TestProtoElementInjector extends ProtoElementInjector { super(parent, index, bindings, firstBindingIsComponent); } - instantiate(parent:ElementInjector, host:ElementInjector):ElementInjector { + instantiate(parent:ElementInjector, host:ElementInjector, events):ElementInjector { this.parentElementInjector = parent; this.hostElementInjector = host; - return super.instantiate(parent, host); + return super.instantiate(parent, host, events); } } diff --git a/modules/core/test/compiler/viewport_spec.js b/modules/core/test/compiler/viewport_spec.js index be9d8099ad..c2411baf53 100644 --- a/modules/core/test/compiler/viewport_spec.js +++ b/modules/core/test/compiler/viewport_spec.js @@ -69,7 +69,7 @@ export function main() { var insertionElement = dom.childNodes[1]; parentView = createView([dom.childNodes[0]]); protoView = new ProtoView(el('
hi
'), new ProtoChangeDetector()); - elementInjector = new ElementInjector(null, null, null); + elementInjector = new ElementInjector(null, null, null, null); viewPort = new ViewPort(parentView, insertionElement, protoView, elementInjector); customViewWithOneNode = createView([el('
single
')]); customViewWithTwoNodes = createView([el('
one
'), el('
two
')]);