From 883e1c1541faf1fe9433b5110392caf7134b6fc8 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Thu, 16 Apr 2015 18:03:15 +0200 Subject: [PATCH] feat(events): support preventdefault Fixes #1039 Closes #1397 --- modules/angular2/docs/core/01_templates.md | 3 +- .../src/core/annotations/annotations.js | 1 + modules/angular2/src/core/compiler/view.js | 16 +++++--- modules/angular2/src/render/dom/view/view.js | 9 +++- .../test/core/compiler/integration_spec.js | 41 +++++++++++++++++++ 5 files changed, 61 insertions(+), 9 deletions(-) diff --git a/modules/angular2/docs/core/01_templates.md b/modules/angular2/docs/core/01_templates.md index 8958ba33a7..d0d501df6b 100644 --- a/modules/angular2/docs/core/01_templates.md +++ b/modules/angular2/docs/core/01_templates.md @@ -514,7 +514,8 @@ Where: * `some-element` Any element which can generate DOM events (or has an angular directive which generates the event). * `some-event` (escaped with `()` or `bind-`) is the name of the event `some-event`. In this case the dash-case is converted into camel-case `someEvent`. -* `statement` is a valid statement (as defined in section below). +* `statement` is a valid statement (as defined in section below). +If the execution of the statement returns `false`, then `preventDefault`is applied on the DOM event. By default, angular only listens to the element on the event, and ignores events which bubble. To listen to bubbled events (as in the case of clicking on any child) use the bubble option (`(^event)` or `on-bubble-event`) as shown diff --git a/modules/angular2/src/core/annotations/annotations.js b/modules/angular2/src/core/annotations/annotations.js index bfc4cac67a..c8beca68f0 100644 --- a/modules/angular2/src/core/annotations/annotations.js +++ b/modules/angular2/src/core/annotations/annotations.js @@ -391,6 +391,7 @@ export class Directive extends Injectable { * * - `event1`: the DOM event that the directive listens to. * - `statement`: the statement to execute when the event occurs. + * If the evalutation of the statement returns `false`, then `preventDefault`is applied on the DOM event. * * To listen to global events, a target must be added to the event name. * The target can be `window`, `document` or `body`. diff --git a/modules/angular2/src/core/compiler/view.js b/modules/angular2/src/core/compiler/view.js index 5bbca54a1e..272bcad939 100644 --- a/modules/angular2/src/core/compiler/view.js +++ b/modules/angular2/src/core/compiler/view.js @@ -131,17 +131,17 @@ export class AppView { } // implementation of EventDispatcher#dispatchEvent - dispatchEvent( - elementIndex:number, eventName:string, locals:Map - ):void { + // returns false if preventDefault must be applied to the DOM event + dispatchEvent(elementIndex:number, eventName:string, locals:Map): boolean { // 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. + var allowDefaultBehavior = true; if (this.hydrated()) { var elBinder = this.proto.elementBinders[elementIndex]; - if (isBlank(elBinder.hostListeners)) return; + if (isBlank(elBinder.hostListeners)) return allowDefaultBehavior; var eventMap = elBinder.hostListeners[eventName]; - if (isBlank(eventMap)) return; + if (isBlank(eventMap)) return allowDefaultBehavior; MapWrapper.forEach(eventMap, (expr, directiveIndex) => { var context; if (directiveIndex === -1) { @@ -149,9 +149,13 @@ export class AppView { } else { context = this.elementInjectors[elementIndex].getDirectiveAtIndex(directiveIndex); } - expr.eval(context, new Locals(this.locals, locals)); + var result = expr.eval(context, new Locals(this.locals, locals)); + if (isPresent(result)) { + allowDefaultBehavior = allowDefaultBehavior && result; + } }); } + return allowDefaultBehavior; } } diff --git a/modules/angular2/src/render/dom/view/view.js b/modules/angular2/src/render/dom/view/view.js index d886c15d7d..1510129dcf 100644 --- a/modules/angular2/src/render/dom/view/view.js +++ b/modules/angular2/src/render/dom/view/view.js @@ -84,7 +84,8 @@ export class RenderView { this._eventDispatcher = dispatcher; } - dispatchEvent(elementIndex, eventName, event) { + dispatchEvent(elementIndex, eventName, event): boolean { + var allowDefaultBehavior = true; if (isPresent(this._eventDispatcher)) { var evalLocals = MapWrapper.create(); MapWrapper.set(evalLocals, '$event', event); @@ -92,7 +93,11 @@ export class RenderView { // out of action expressions // var localValues = this.proto.elementBinders[elementIndex].eventLocals.eval(null, new Locals(null, evalLocals)); // this._eventDispatcher.dispatchEvent(elementIndex, eventName, localValues); - this._eventDispatcher.dispatchEvent(elementIndex, eventName, evalLocals); + allowDefaultBehavior = this._eventDispatcher.dispatchEvent(elementIndex, eventName, evalLocals); + if (!allowDefaultBehavior) { + event.preventDefault(); + } } + return allowDefaultBehavior; } } diff --git a/modules/angular2/test/core/compiler/integration_spec.js b/modules/angular2/test/core/compiler/integration_spec.js index 9ed8a40f41..ed57d5fd16 100644 --- a/modules/angular2/test/core/compiler/integration_spec.js +++ b/modules/angular2/test/core/compiler/integration_spec.js @@ -591,6 +591,23 @@ export function main() { }); })); + it('should support preventing default on render events', inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView(MyComp, new View({ + template: '', + directives: [DecoratorListeningDomEventPrevent, DecoratorListeningDomEventNoPrevent] + })); + + tb.createView(MyComp, {context: ctx}).then((view) => { + expect(DOM.getChecked(view.rootNodes[0])).toBeFalsy(); + expect(DOM.getChecked(view.rootNodes[1])).toBeFalsy(); + DOM.dispatchEvent(view.rootNodes[0], DOM.createMouseEvent('click')); + DOM.dispatchEvent(view.rootNodes[1], DOM.createMouseEvent('click')); + expect(DOM.getChecked(view.rootNodes[0])).toBeFalsy(); + expect(DOM.getChecked(view.rootNodes[1])).toBeTruthy(); + async.done(); + }); + })); + it('should support render global events from multiple directives', inject([TestBed, AsyncTestCompleter], (tb, async) => { tb.overrideView(MyComp, new View({ template: '
', @@ -1162,6 +1179,30 @@ class DecoratorListeningDomEventOther { } } +@Decorator({ + selector: '[listenerprevent]', + hostListeners: { + 'click': 'onEvent($event)' + } +}) +class DecoratorListeningDomEventPrevent { + onEvent(event) { + return false; + } +} + +@Decorator({ + selector: '[listenernoprevent]', + hostListeners: { + 'click': 'onEvent($event)' + } +}) +class DecoratorListeningDomEventNoPrevent { + onEvent(event) { + return true; + } +} + @Component({ selector: '[id]', properties: {'id': 'id'}