feat(events): add support for global events

Fixes #1098
Closes #1255
This commit is contained in:
Marc Laval
2015-04-02 15:56:58 +02:00
parent 7c95cea3a8
commit b96e560c8d
27 changed files with 414 additions and 103 deletions

View File

@ -11,8 +11,7 @@ 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 {BindingPropagationConfig, Parser, Lexer} from 'angular2/change_detection';
import {ViewRef, Renderer} from 'angular2/src/render/api';
import {ViewRef, Renderer, EventBinding} from 'angular2/src/render/api';
import {QueryList} from 'angular2/src/core/compiler/query_list';
class DummyDirective extends Directive {
@ -701,7 +700,9 @@ export function main() {
StringMapWrapper.set(handlers, eventName, eventHandler);
var pv = new AppProtoView(null, null, null);
pv.bindElement(null, 0, null, null, null);
pv.bindEvent(eventName, new Parser(new Lexer()).parseAction('handler()', ''));
var eventBindings = ListWrapper.create();
ListWrapper.push(eventBindings, new EventBinding(eventName, new Parser(new Lexer()).parseAction('handler()', '')));
pv.bindEvent(eventBindings);
var view = new AppView(pv, MapWrapper.create());
view.context = new ContextWithHandler(eventHandler);

View File

@ -17,7 +17,7 @@ import {
import {TestBed} from 'angular2/src/test_lib/test_bed';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Type, isPresent, BaseException, assertionsEnabled, isJsObject} from 'angular2/src/facade/lang';
import {Type, isPresent, BaseException, assertionsEnabled, isJsObject, global} from 'angular2/src/facade/lang';
import {PromiseWrapper} from 'angular2/src/facade/async';
import {Injector, bind} from 'angular2/di';
@ -541,6 +541,66 @@ export function main() {
async.done();
});
}));
it('should support render global events', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '<div listener></div>',
directives: [DecoratorListeningDomEvent]
}));
tb.createView(MyComp, {context: ctx}).then((view) => {
var injector = view.rawView.elementInjectors[0];
var listener = injector.get(DecoratorListeningDomEvent);
dispatchEvent(DOM.getGlobalEventTarget("window"), 'domEvent');
expect(listener.eventType).toEqual('window_domEvent');
listener = injector.get(DecoratorListeningDomEvent);
dispatchEvent(DOM.getGlobalEventTarget("document"), 'domEvent');
expect(listener.eventType).toEqual('document_domEvent');
view.rawView.dehydrate();
listener = injector.get(DecoratorListeningDomEvent);
dispatchEvent(DOM.getGlobalEventTarget("body"), 'domEvent');
expect(listener.eventType).toEqual('');
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>',
directives: [If, DecoratorListeningDomEvent, DecoratorListeningDomEventOther]
}));
tb.createView(MyComp, {context: ctx}).then((view) => {
globalCounter = 0;
ctx.ctxBoolProp = true;
view.detectChanges();
var subview = view.rawView.viewContainers[0].get(0);
var injector = subview.elementInjectors[0];
var listener = injector.get(DecoratorListeningDomEvent);
var listenerother = injector.get(DecoratorListeningDomEventOther);
dispatchEvent(DOM.getGlobalEventTarget("window"), 'domEvent');
expect(listener.eventType).toEqual('window_domEvent');
expect(listenerother.eventType).toEqual('other_domEvent');
expect(globalCounter).toEqual(1);
ctx.ctxBoolProp = false;
view.detectChanges();
dispatchEvent(DOM.getGlobalEventTarget("window"), 'domEvent');
expect(globalCounter).toEqual(1);
ctx.ctxBoolProp = true;
view.detectChanges();
dispatchEvent(DOM.getGlobalEventTarget("window"), 'domEvent');
expect(globalCounter).toEqual(2);
async.done();
});
}));
}
describe("dynamic components", () => {
@ -894,18 +954,49 @@ class DecoratorListeningEvent {
@Decorator({
selector: '[listener]',
hostListeners: {'domEvent': 'onEvent($event.type)'}
hostListeners: {
'domEvent': 'onEvent($event.type)',
'window:domEvent': 'onWindowEvent($event.type)',
'document:domEvent': 'onDocumentEvent($event.type)',
'body:domEvent': 'onBodyEvent($event.type)'
}
})
class DecoratorListeningDomEvent {
eventType: string;
constructor() {
this.eventType = '';
}
onEvent(eventType: string) {
this.eventType = eventType;
}
onWindowEvent(eventType: string) {
this.eventType = "window_" + eventType;
}
onDocumentEvent(eventType: string) {
this.eventType = "document_" + eventType;
}
onBodyEvent(eventType: string) {
this.eventType = "body_" + eventType;
}
}
var globalCounter = 0;
@Decorator({
selector: '[listenerother]',
hostListeners: {
'window:domEvent': 'onEvent($event.type)'
}
})
class DecoratorListeningDomEventOther {
eventType: string;
counter: int;
constructor() {
this.eventType = '';
}
onEvent(eventType: string) {
globalCounter++;
this.eventType = "other_" + eventType;
}
}
@Component({

View File

@ -23,7 +23,8 @@ export function main() {
someDecorator,
someDecoratorIgnoringChildren,
someDecoratorWithProps,
someDecoratorWithEvents
someDecoratorWithEvents,
someDecoratorWithGlobalEvents
];
parser = new Parser(new Lexer());
});
@ -130,8 +131,21 @@ export function main() {
el('<div some-decor-events></div>')
);
var directiveBinding = results[0].directives[0];
expect(MapWrapper.get(directiveBinding.eventBindings, 'click').source)
.toEqual('doIt()');
expect(directiveBinding.eventBindings.length).toEqual(1);
var eventBinding = directiveBinding.eventBindings[0];
expect(eventBinding.fullName).toEqual('click');
expect(eventBinding.source.source).toEqual('doIt()');
});
it('should bind directive global events', () => {
var results = process(
el('<div some-decor-globalevents></div>')
);
var directiveBinding = results[0].directives[0];
expect(directiveBinding.eventBindings.length).toEqual(1);
var eventBinding = directiveBinding.eventBindings[0];
expect(eventBinding.fullName).toEqual('window:resize');
expect(eventBinding.source.source).toEqual('doItGlobal()');
});
describe('viewport directives', () => {
@ -246,3 +260,10 @@ var someDecoratorWithEvents = new DirectiveMetadata({
'click': 'doIt()'
})
});
var someDecoratorWithGlobalEvents = new DirectiveMetadata({
selector: '[some-decor-globalevents]',
hostListeners: MapWrapper.createFromStringMap({
'window:resize': 'doItGlobal()'
})
});

View File

@ -96,10 +96,14 @@ export function main() {
it('should detect () syntax', () => {
var results = process(el('<div (click)="b()"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('b()');
var eventBinding = results[0].eventBindings[0];
expect(eventBinding.source.source).toEqual('b()');
expect(eventBinding.fullName).toEqual('click');
// "(click[])" is not an expected syntax and is only used to validate the regexp
results = process(el('<div (click[])="b()"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click[]').source).toEqual('b()');
eventBinding = results[0].eventBindings[0];
expect(eventBinding.source.source).toEqual('b()');
expect(eventBinding.fullName).toEqual('click[]');
});
it('should detect () syntax only if an attribute name starts and ends with ()', () => {
@ -109,17 +113,23 @@ export function main() {
it('should parse event handlers using () syntax as actions', () => {
var results = process(el('<div (click)="foo=bar"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('foo=bar');
var eventBinding = results[0].eventBindings[0];
expect(eventBinding.source.source).toEqual('foo=bar');
expect(eventBinding.fullName).toEqual('click');
});
it('should detect on- syntax', () => {
var results = process(el('<div on-click="b()"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('b()');
var eventBinding = results[0].eventBindings[0];
expect(eventBinding.source.source).toEqual('b()');
expect(eventBinding.fullName).toEqual('click');
});
it('should parse event handlers using on- syntax as actions', () => {
var results = process(el('<div on-click="foo=bar"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('foo=bar');
var eventBinding = results[0].eventBindings[0];
expect(eventBinding.source.source).toEqual('foo=bar');
expect(eventBinding.fullName).toEqual('click');
});
it('should store bound properties as temporal attributes', () => {

View File

@ -83,6 +83,28 @@ export function main() {
expect(receivedEvent).toBe(dispatchedEvent);
});
it('should add and remove global event listeners with correct bubbling', () => {
var element = el('<div><div></div></div>');
DOM.appendChild(document.body, element);
var dispatchedEvent = DOM.createMouseEvent('click');
var receivedEvent = null;
var handler = (e) => { receivedEvent = e; };
var manager = new EventManager([domEventPlugin], new FakeVmTurnZone());
var remover = manager.addGlobalEventListener("document", '^click', handler);
DOM.dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(dispatchedEvent);
receivedEvent = null;
remover();
DOM.dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(null);
remover = manager.addGlobalEventListener("document", 'click', handler);
DOM.dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(null);
});
});
}
@ -104,6 +126,8 @@ class FakeEventManagerPlugin extends EventManagerPlugin {
addEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean) {
MapWrapper.set(shouldSupportBubble ? this._bubbleEventHandlers : this._nonBubbleEventHandlers,
eventName, handler);
return () => {MapWrapper.delete(shouldSupportBubble ? this._bubbleEventHandlers : this._nonBubbleEventHandlers,
eventName)};
}
}
@ -117,6 +141,6 @@ class FakeVmTurnZone extends VmTurnZone {
}
runOutsideAngular(fn) {
fn();
return fn();
}
}

View File

@ -173,6 +173,7 @@ export class FakeEventManagerPlugin extends EventManagerPlugin {
addEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean) {
MapWrapper.set(this._eventHandlers, eventName, handler);
return () => {MapWrapper.delete(this._eventHandlers, eventName);}
}
}

View File

@ -47,7 +47,7 @@ export function main() {
it('should attach the view nodes as child of the host element', () => {
var host = el('<div><span>original content</span></div>');
var nodes = el('<div>view</div>');
var view = new RenderView(null, [nodes], [], [], [], []);
var view = new RenderView(null, [nodes], [], [], [], [], null);
strategy.attachTemplate(host, view);
var firstChild = DOM.firstChild(host);

View File

@ -42,7 +42,7 @@ export function main() {
it('should attach the view nodes as child of the host element', () => {
var host = el('<div><span>original content</span></div>');
var nodes = el('<div>view</div>');
var view = new RenderView(null, [nodes], [], [], [], []);
var view = new RenderView(null, [nodes], [], [], [], [], null);
strategy.attachTemplate(host, view);
var firstChild = DOM.firstChild(host);

View File

@ -35,7 +35,7 @@ export function main() {
it('should attach the view nodes to the shadow root', () => {
var host = el('<div><span>original content</span></div>');
var nodes = el('<div>view</div>');
var view = new RenderView(null, [nodes], [], [], [], []);
var view = new RenderView(null, [nodes], [], [], [], [], null);
strategy.attachTemplate(host, view);
var shadowRoot = DOM.getShadowRoot(host);

View File

@ -2,6 +2,7 @@ import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} fr
import {ListWrapper} from 'angular2/src/facade/collection';
import {RenderProtoView} from 'angular2/src/render/dom/view/proto_view';
import {RenderView} from 'angular2/src/render/dom/view/view';
import {ShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/shadow_dom_strategy';
import {LightDom} from 'angular2/src/render/dom/shadow_dom/light_dom';
@ -9,14 +10,15 @@ import {LightDom} from 'angular2/src/render/dom/shadow_dom/light_dom';
export function main() {
function createView() {
var proto = null;
var proto = new RenderProtoView({element: el('<div></div>'), isRootView: false, elementBinders: []});
var rootNodes = [el('<div></div>')];
var boundTextNodes = [];
var boundElements = [el('<div></div>')];
var viewContainers = [];
var contentTags = [];
var eventManager = null;
return new RenderView(proto, rootNodes,
boundTextNodes, boundElements, viewContainers, contentTags);
boundTextNodes, boundElements, viewContainers, contentTags, eventManager);
}
function createShadowDomStrategy(log) {