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
')]);