feat(events): support preventdefault

Fixes #1039
Closes #1397
This commit is contained in:
Marc Laval 2015-04-16 18:03:15 +02:00
parent aabe83cf63
commit 883e1c1541
5 changed files with 61 additions and 9 deletions

View File

@ -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

View File

@ -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`.

View File

@ -131,17 +131,17 @@ export class AppView {
}
// implementation of EventDispatcher#dispatchEvent
dispatchEvent(
elementIndex:number, eventName:string, locals:Map<string, any>
):void {
// returns false if preventDefault must be applied to the DOM event
dispatchEvent(elementIndex:number, eventName:string, locals:Map<string, any>): 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;
}
}

View File

@ -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;
}
}

View File

@ -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: '<input type="checkbox" listenerprevent></input><input type="checkbox" listenernoprevent></input>',
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: '<div *if="ctxBoolProp" listener listenerother></div>',
@ -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'}