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

@ -367,6 +367,8 @@ export class Directive extends Injectable {
* - `event1`: the DOM event that the directive listens to.
* - `statement`: the statement to execute when the event occurs.
*
* To listen to global events, a target must be added to the event name.
* The target can be `window`, `document` or `body`.
*
* When writing a directive event binding, you can also refer to the following local variables:
* - `$event`: Current event object which triggered the event.
@ -380,6 +382,7 @@ export class Directive extends Injectable {
* @Directive({
* hostListeners: {
* 'event1': 'onMethod1(arguments)',
* 'target:event2': 'onMethod2(arguments)',
* ...
* }
* }
@ -387,19 +390,22 @@ export class Directive extends Injectable {
*
* ## Basic Event Binding:
*
* Suppose you want to write a directive that triggers on `change` hostListeners in the DOM. You would define the event
* binding as follows:
* Suppose you want to write a directive that triggers on `change` events in the DOM and on `resize` events in window.
* You would define the event binding as follows:
*
* ```
* @Decorator({
* selector: 'input',
* hostListeners: {
* 'change': 'onChange($event)'
* 'change': 'onChange($event)',
* 'window:resize': 'onResize($event)'
* }
* })
* class InputDecorator {
* onChange(event:Event) {
* }
* onResize(event:Event) {
* }
* }
* ```
*

View File

@ -118,9 +118,7 @@ export class ProtoViewFactory {
protoView.bindElementProperty(astWithSource.ast, propertyName);
});
// events
MapWrapper.forEach(renderElementBinder.eventBindings, (astWithSource, eventName) => {
protoView.bindEvent(eventName, astWithSource.ast, -1);
});
protoView.bindEvent(renderElementBinder.eventBindings, -1);
// variables
// The view's locals needs to have a full set of variable names at construction time
// in order to prevent new variables from being set later in the lifecycle. Since we don't want
@ -143,9 +141,7 @@ export class ProtoViewFactory {
protoView.bindDirectiveProperty(i, astWithSource.ast, propertyName, setter);
});
// directive events
MapWrapper.forEach(renderDirectiveMetadata.eventBindings, (astWithSource, eventName) => {
protoView.bindEvent(eventName, astWithSource.ast, i);
});
protoView.bindEvent(renderDirectiveMetadata.eventBindings, i);
}
}

View File

@ -272,7 +272,7 @@ export class AppView {
var elBinder = this.proto.elementBinders[elementIndex];
if (isBlank(elBinder.hostListeners)) return;
var eventMap = elBinder.hostListeners[eventName];
if (isBlank(eventName)) return;
if (isBlank(eventMap)) return;
MapWrapper.forEach(eventMap, (expr, directiveIndex) => {
var context;
if (directiveIndex === -1) {
@ -407,19 +407,23 @@ export class AppProtoView {
* @param {int} directiveIndex The directive index in the binder or -1 when the event is not bound
* to a directive
*/
bindEvent(eventName:string, expression:AST, directiveIndex: int = -1) {
bindEvent(eventBindings: List<renderApi.EventBinding>, directiveIndex: int = -1) {
var elBinder = this.elementBinders[this.elementBinders.length - 1];
var events = elBinder.hostListeners;
if (isBlank(events)) {
events = StringMapWrapper.create();
elBinder.hostListeners = events;
}
var event = StringMapWrapper.get(events, eventName);
if (isBlank(event)) {
event = MapWrapper.create();
StringMapWrapper.set(events, eventName, event);
for (var i = 0; i < eventBindings.length; i++) {
var eventBinding = eventBindings[i];
var eventName = eventBinding.fullName;
var event = StringMapWrapper.get(events, eventName);
if (isBlank(event)) {
event = MapWrapper.create();
StringMapWrapper.set(events, eventName, event);
}
MapWrapper.set(event, directiveIndex, eventBinding.source);
}
MapWrapper.set(event, directiveIndex, expression);
}
/**

View File

@ -119,6 +119,12 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
// addEventListener misses zones so we use element.on.
element.on[event].listen(callback);
}
Function onAndCancel(EventTarget element, String event, callback(arg)) {
// due to https://code.google.com/p/dart/issues/detail?id=17406
// addEventListener misses zones so we use element.on.
var subscription = element.on[event].listen(callback);
return subscription.cancel;
}
void dispatchEvent(EventTarget el, Event evt) {
el.dispatchEvent(evt);
}
@ -288,4 +294,13 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
int keyCode = event.keyCode;
return _keyCodeToKeyMap.containsKey(keyCode) ? _keyCodeToKeyMap[keyCode] : 'Unidentified';
}
getGlobalEventTarget(String target) {
if (target == "window") {
return window;
} else if (target == "document") {
return document;
} else if (target == "body") {
return document.body;
}
}
}

View File

@ -73,6 +73,12 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
on(el, evt, listener) {
el.addEventListener(evt, listener, false);
}
onAndCancel(el, evt, listener): Function {
el.addEventListener(evt, listener, false);
//Needed to follow Dart's subscription semantic, until fix of
//https://code.google.com/p/dart/issues/detail?id=17406
return () => {el.removeEventListener(evt, listener, false);};
}
dispatchEvent(el, evt) {
el.dispatchEvent(evt);
}
@ -353,4 +359,13 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
}
return key;
}
getGlobalEventTarget(target:string) {
if (target == "window") {
return window;
} else if (target == "document") {
return document;
} else if (target == "body") {
return document.body;
}
}
}

View File

@ -39,6 +39,9 @@ export class DomAdapter {
on(el, evt, listener) {
throw _abstract();
}
onAndCancel(el, evt, listener): Function {
throw _abstract();
}
dispatchEvent(el, evt) {
throw _abstract();
}
@ -273,4 +276,7 @@ export class DomAdapter {
supportsNativeShadowDOM(): boolean {
throw _abstract();
}
getGlobalEventTarget(target:string) {
throw _abstract();
}
}

View File

@ -29,6 +29,9 @@ class Html5LibDomAdapter implements DomAdapter {
on(el, evt, listener) {
throw 'not implemented';
}
Function onAndCancel(el, evt, listener) {
throw 'not implemented';
}
dispatchEvent(el, evt) {
throw 'not implemented';
}

View File

@ -86,6 +86,9 @@ export class Parse5DomAdapter extends DomAdapter {
on(el, evt, listener) {
//Do nothing, in order to not break forms integration tests
}
onAndCancel(el, evt, listener): Function {
//Do nothing, in order to not break forms integration tests
}
dispatchEvent(el, evt) {
throw _notImplemented('dispatchEvent');
}

View File

@ -10,6 +10,6 @@ export class MockVmTurnZone extends VmTurnZone {
}
runOutsideAngular(fn) {
fn();
return fn();
}
}

View File

@ -15,6 +15,16 @@ import {ASTWithSource} from 'angular2/change_detection';
* - render compiler is not on the critical path as
* its output will be stored in precompiled templates.
*/
export class EventBinding {
fullName: string; // name/target:name, e.g "click", "window:resize"
source: ASTWithSource;
constructor(fullName :string, source: ASTWithSource) {
this.fullName = fullName;
this.source = source;
}
}
export class ElementBinder {
index:number;
parentIndex:number;
@ -26,7 +36,7 @@ export class ElementBinder {
// Note: this contains a preprocessed AST
// that replaced the values that should be extracted from the element
// with a local name
eventBindings: Map<string, ASTWithSource>;
eventBindings: List<EventBinding>;
textBindings: List<ASTWithSource>;
readAttributes: Map<string, string>;
@ -57,7 +67,7 @@ export class DirectiveBinder {
// Note: this contains a preprocessed AST
// that replaced the values that should be extracted from the element
// with a local name
eventBindings: Map<string, ASTWithSource>;
eventBindings: List<EventBinding>;
constructor({
directiveIndex, propertyBindings, eventBindings
}) {

View File

@ -1,4 +1,4 @@
import {isPresent, isBlank, BaseException, assertionsEnabled, RegExpWrapper} from 'angular2/src/facade/lang';
import {isPresent, isBlank, BaseException, assertionsEnabled, RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
import {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Parser} from 'angular2/change_detection';
@ -10,7 +10,7 @@ import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
import {DirectiveMetadata} from '../../api';
import {dashCaseToCamelCase, camelCaseToDashCase} from '../util';
import {dashCaseToCamelCase, camelCaseToDashCase, EVENT_TARGET_SEPARATOR} from '../util';
/**
* Parses the directives on a single element. Assumes ViewSplitter has already created
@ -132,7 +132,13 @@ export class DirectiveParser extends CompileStep {
_bindDirectiveEvent(eventName, action, compileElement, directiveBinder) {
var ast = this._parser.parseAction(action, compileElement.elementDescription);
directiveBinder.bindEvent(eventName, ast);
if (StringWrapper.contains(eventName, EVENT_TARGET_SEPARATOR)) {
var parts = eventName.split(EVENT_TARGET_SEPARATOR);
directiveBinder.bindEvent(parts[1], ast, parts[0]);
} else {
directiveBinder.bindEvent(eventName, ast);
}
}
_splitBindConfig(bindConfig:string) {

View File

@ -18,13 +18,15 @@ export class EventManager {
}
addEventListener(element, eventName: string, handler: Function) {
var shouldSupportBubble = eventName[0] == BUBBLE_SYMBOL;
if (shouldSupportBubble) {
eventName = StringWrapper.substring(eventName, 1);
}
var withoutBubbleSymbol = this._removeBubbleSymbol(eventName);
var plugin = this._findPluginFor(withoutBubbleSymbol);
plugin.addEventListener(element, withoutBubbleSymbol, handler, withoutBubbleSymbol != eventName);
}
var plugin = this._findPluginFor(eventName);
plugin.addEventListener(element, eventName, handler, shouldSupportBubble);
addGlobalEventListener(target: string, eventName: string, handler: Function): Function {
var withoutBubbleSymbol = this._removeBubbleSymbol(eventName);
var plugin = this._findPluginFor(withoutBubbleSymbol);
return plugin.addGlobalEventListener(target, withoutBubbleSymbol, handler, withoutBubbleSymbol != eventName);
}
getZone(): VmTurnZone {
@ -41,6 +43,10 @@ export class EventManager {
}
throw new BaseException(`No event manager plugin found for event ${eventName}`);
}
_removeBubbleSymbol(eventName: string): string {
return eventName[0] == BUBBLE_SYMBOL ? StringWrapper.substring(eventName, 1) : eventName;
}
}
export class EventManagerPlugin {
@ -54,8 +60,11 @@ export class EventManagerPlugin {
return false;
}
addEventListener(element, eventName: string, handler: Function,
shouldSupportBubble: boolean) {
addEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean) {
throw "not implemented";
}
addGlobalEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean): Function {
throw "not implemented";
}
}
@ -69,17 +78,27 @@ export class DomEventsPlugin extends EventManagerPlugin {
return true;
}
addEventListener(element, eventName: string, handler: Function,
shouldSupportBubble: boolean) {
var outsideHandler = shouldSupportBubble ?
DomEventsPlugin.bubbleCallback(element, handler, this.manager._zone) :
DomEventsPlugin.sameElementCallback(element, handler, this.manager._zone);
addEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean) {
var outsideHandler = this._getOutsideHandler(shouldSupportBubble, element, handler, this.manager._zone);
this.manager._zone.runOutsideAngular(() => {
DOM.on(element, eventName, outsideHandler);
});
}
addGlobalEventListener(target:string, eventName: string, handler: Function, shouldSupportBubble: boolean): Function {
var element = DOM.getGlobalEventTarget(target);
var outsideHandler = this._getOutsideHandler(shouldSupportBubble, element, handler, this.manager._zone);
return this.manager._zone.runOutsideAngular(() => {
return DOM.onAndCancel(element, eventName, outsideHandler);
});
}
_getOutsideHandler(shouldSupportBubble: boolean, element, handler: Function, zone: VmTurnZone) {
return shouldSupportBubble ?
DomEventsPlugin.bubbleCallback(element, handler, zone) :
DomEventsPlugin.sameElementCallback(element, handler, zone);
}
static sameElementCallback(element, handler, zone) {
return (event) => {
if (event.target === element) {

View File

@ -3,6 +3,8 @@ import {StringWrapper, RegExpWrapper, isPresent} from 'angular2/src/facade/lang'
export const NG_BINDING_CLASS_SELECTOR = '.ng-binding';
export const NG_BINDING_CLASS = 'ng-binding';
export const EVENT_TARGET_SEPARATOR = ':';
var CAMEL_CASE_REGEXP = RegExpWrapper.create('([A-Z])');
var DASH_CASE_REGEXP = RegExpWrapper.create('-([a-z])');

View File

@ -8,7 +8,8 @@ export class ElementBinder {
textNodeIndices: List<number>;
nestedProtoView: protoViewModule.RenderProtoView;
eventLocals: AST;
eventNames: List<string>;
localEvents: List<Event>;
globalEvents: List<Event>;
componentId: string;
parentIndex:number;
distanceToParent:number;
@ -20,7 +21,8 @@ export class ElementBinder {
nestedProtoView,
componentId,
eventLocals,
eventNames,
localEvents,
globalEvents,
parentIndex,
distanceToParent,
propertySetters
@ -30,9 +32,22 @@ export class ElementBinder {
this.nestedProtoView = nestedProtoView;
this.componentId = componentId;
this.eventLocals = eventLocals;
this.eventNames = eventNames;
this.localEvents = localEvents;
this.globalEvents = globalEvents;
this.parentIndex = parentIndex;
this.distanceToParent = distanceToParent;
this.propertySetters = propertySetters;
}
}
export class Event {
name: string;
target: string;
fullName: string;
constructor(name: string, target: string, fullName: string) {
this.name = name;
this.target = target;
this.fullName = fullName;
}
}

View File

@ -1,5 +1,5 @@
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, Set, SetWrapper} from 'angular2/src/facade/collection';
import {ListWrapper, MapWrapper, Set, SetWrapper, List} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {
@ -8,13 +8,13 @@ import {
import {SetterFn} from 'angular2/src/reflection/types';
import {RenderProtoView} from './proto_view';
import {ElementBinder} from './element_binder';
import {ElementBinder, Event} from './element_binder';
import {setterFactory} from './property_setter_factory';
import * as api from '../../api';
import * as directDomRenderer from '../direct_dom_renderer';
import {NG_BINDING_CLASS} from '../util';
import {NG_BINDING_CLASS, EVENT_TARGET_SEPARATOR} from '../util';
export class ProtoViewBuilder {
rootElement;
@ -56,12 +56,12 @@ export class ProtoViewBuilder {
var apiElementBinders = [];
ListWrapper.forEach(this.elements, (ebb) => {
var propertySetters = MapWrapper.create();
var eventLocalsAstSplitter = new EventLocalsAstSplitter();
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (db) => {
ebb.eventBuilder.merge(db.eventBuilder);
return new api.DirectiveBinder({
directiveIndex: db.directiveIndex,
propertyBindings: db.propertyBindings,
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(db.eventBindings)
eventBindings: db.eventBindings
});
});
MapWrapper.forEach(ebb.propertySetters, (setter, propertyName) => {
@ -75,7 +75,7 @@ export class ProtoViewBuilder {
directives: apiDirectiveBinders,
nestedProtoView: nestedProtoView,
propertyBindings: ebb.propertyBindings, variableBindings: ebb.variableBindings,
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(ebb.eventBindings),
eventBindings: ebb.eventBindings,
textBindings: ebb.textBindings,
readAttributes: ebb.readAttributes
}));
@ -86,8 +86,9 @@ export class ProtoViewBuilder {
distanceToParent: ebb.distanceToParent,
nestedProtoView: isPresent(nestedProtoView) ? nestedProtoView.render.delegate : null,
componentId: ebb.componentId,
eventLocals: eventLocalsAstSplitter.buildEventLocals(),
eventNames: eventLocalsAstSplitter.buildEventNames(),
eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()),
localEvents: ebb.eventBuilder.buildLocalEvents(),
globalEvents: ebb.eventBuilder.buildGlobalEvents(),
propertySetters: propertySetters
}));
});
@ -112,7 +113,8 @@ export class ElementBinderBuilder {
nestedProtoView:ProtoViewBuilder;
propertyBindings: Map<string, ASTWithSource>;
variableBindings: Map<string, string>;
eventBindings: Map<string, ASTWithSource>;
eventBindings: List<api.EventBinding>;
eventBuilder: EventBuilder;
textBindingIndices: List<number>;
textBindings: List<ASTWithSource>;
contentTagSelector:string;
@ -129,7 +131,8 @@ export class ElementBinderBuilder {
this.nestedProtoView = null;
this.propertyBindings = MapWrapper.create();
this.variableBindings = MapWrapper.create();
this.eventBindings = MapWrapper.create();
this.eventBindings = ListWrapper.create();
this.eventBuilder = new EventBuilder();
this.textBindings = [];
this.textBindingIndices = [];
this.contentTagSelector = null;
@ -191,8 +194,8 @@ export class ElementBinderBuilder {
}
}
bindEvent(name, expression) {
MapWrapper.set(this.eventBindings, name, expression);
bindEvent(name, expression, target = null) {
ListWrapper.push(this.eventBindings, this.eventBuilder.add(name, expression, target));
}
bindText(index, expression) {
@ -212,49 +215,53 @@ export class ElementBinderBuilder {
export class DirectiveBuilder {
directiveIndex:number;
propertyBindings: Map<string, ASTWithSource>;
eventBindings: Map<string, ASTWithSource>;
eventBindings: List<api.EventBinding>;
eventBuilder: EventBuilder;
constructor(directiveIndex) {
this.directiveIndex = directiveIndex;
this.propertyBindings = MapWrapper.create();
this.eventBindings = MapWrapper.create();
this.eventBindings = ListWrapper.create();
this.eventBuilder = new EventBuilder();
}
bindProperty(name, expression) {
MapWrapper.set(this.propertyBindings, name, expression);
}
bindEvent(name, expression) {
MapWrapper.set(this.eventBindings, name, expression);
bindEvent(name, expression, target = null) {
ListWrapper.push(this.eventBindings, this.eventBuilder.add(name, expression, target));
}
}
export class EventLocalsAstSplitter extends AstTransformer {
locals:List<AST>;
eventNames:List<string>;
_implicitReceiver:AST;
export class EventBuilder extends AstTransformer {
locals: List<AST>;
localEvents: List<Event>;
globalEvents: List<Event>;
_implicitReceiver: AST;
constructor() {
super();
this.locals = [];
this.eventNames = [];
this.localEvents = [];
this.globalEvents = [];
this._implicitReceiver = new ImplicitReceiver();
}
splitEventAstIntoLocals(eventBindings:Map<string, ASTWithSource>):Map<string, ASTWithSource> {
if (isPresent(eventBindings)) {
var result = MapWrapper.create();
MapWrapper.forEach(eventBindings, (astWithSource, eventName) => {
// TODO(tbosch): reenable this when we are parsing element properties
// out of action expressions
// var adjustedAst = astWithSource.ast.visit(this);
var adjustedAst = astWithSource.ast;
MapWrapper.set(result, eventName, new ASTWithSource(adjustedAst, astWithSource.source, ''));
ListWrapper.push(this.eventNames, eventName);
});
return result;
add(name: string, source: ASTWithSource, target: string): api.EventBinding {
// TODO(tbosch): reenable this when we are parsing element properties
// out of action expressions
// var adjustedAst = astWithSource.ast.visit(this);
var adjustedAst = source.ast;
var fullName = isPresent(target) ? target + EVENT_TARGET_SEPARATOR + name : name;
var result = new api.EventBinding(fullName, new ASTWithSource(adjustedAst, source.source, ''));
var event = new Event(name, target, fullName);
if (isBlank(target)) {
ListWrapper.push(this.localEvents, event);
} else {
ListWrapper.push(this.globalEvents, event);
}
return null;
return result;
}
visitAccessMember(ast:AccessMember) {
@ -277,10 +284,32 @@ export class EventLocalsAstSplitter extends AstTransformer {
}
buildEventLocals() {
return new LiteralArray(this.locals);
return this.locals;
}
buildEventNames() {
return this.eventNames;
buildLocalEvents() {
return this.localEvents;
}
buildGlobalEvents() {
return this.globalEvents;
}
merge(eventBuilder: EventBuilder) {
this._merge(this.localEvents, eventBuilder.localEvents);
this._merge(this.globalEvents, eventBuilder.globalEvents);
ListWrapper.concat(this.locals, eventBuilder.locals);
}
_merge(host: List<Event>, tobeAdded: List<Event>) {
var names = ListWrapper.create();
for (var i = 0; i < host.length; i++) {
ListWrapper.push(names, host[i].fullName);
}
for (var j = 0; j < tobeAdded.length; j++) {
if (!ListWrapper.contains(names, tobeAdded[j].fullName)) {
ListWrapper.push(host, tobeAdded[j]);
}
}
}
}

View File

@ -6,6 +6,7 @@ import {ViewContainer} from './view_container';
import {RenderProtoView} from './proto_view';
import {LightDom} from '../shadow_dom/light_dom';
import {Content} from '../shadow_dom/content_tag';
import {EventManager} from 'angular2/src/render/dom/events/event_manager';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
@ -29,12 +30,14 @@ export class RenderView {
contentTags: List<Content>;
lightDoms: List<LightDom>;
proto: RenderProtoView;
eventManager: EventManager;
_hydrated: boolean;
_eventDispatcher: any/*EventDispatcher*/;
_eventHandlerRemovers: List<Function>;
constructor(
proto:RenderProtoView, rootNodes:List,
boundTextNodes: List, boundElements:List, viewContainers:List, contentTags:List) {
boundTextNodes: List, boundElements:List, viewContainers:List, contentTags:List, eventManager: EventManager) {
this.proto = proto;
this.rootNodes = rootNodes;
this.boundTextNodes = boundTextNodes;
@ -42,9 +45,11 @@ export class RenderView {
this.viewContainers = viewContainers;
this.contentTags = contentTags;
this.lightDoms = ListWrapper.createFixedSize(boundElements.length);
this.eventManager = eventManager;
ListWrapper.fill(this.lightDoms, null);
this.componentChildViews = ListWrapper.createFixedSize(boundElements.length);
this._hydrated = false;
this._eventHandlerRemovers = null;
}
hydrated() {
@ -130,6 +135,26 @@ export class RenderView {
lightDom.redistribute();
}
}
//add global events
this._eventHandlerRemovers = ListWrapper.create();
var binders = this.proto.elementBinders;
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
var binder = binders[binderIdx];
if (isPresent(binder.globalEvents)) {
for (var i = 0; i < binder.globalEvents.length; i++) {
var globalEvent = binder.globalEvents[i];
var remover = this._createGlobalEventListener(binderIdx, globalEvent.name, globalEvent.target, globalEvent.fullName);
ListWrapper.push(this._eventHandlerRemovers, remover);
}
}
}
}
_createGlobalEventListener(elementIndex, eventName, eventTarget, fullName): Function {
return this.eventManager.addGlobalEventListener(eventTarget, eventName, (event) => {
this.dispatchEvent(elementIndex, fullName, event);
});
}
dehydrate() {
@ -156,6 +181,13 @@ export class RenderView {
}
}
}
//remove global events
for (var i = 0; i < this._eventHandlerRemovers.length; i++) {
this._eventHandlerRemovers[i]();
}
this._eventHandlerRemovers = null;
this._eventDispatcher = null;
this._hydrated = false;
}

View File

@ -125,7 +125,7 @@ export class ViewFactory {
var view = new viewModule.RenderView(
protoView, viewRootNodes,
boundTextNodes, boundElements, viewContainers, contentTags
boundTextNodes, boundElements, viewContainers, contentTags, this._eventManager
);
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
@ -139,10 +139,10 @@ export class ViewFactory {
}
// events
if (isPresent(binder.eventLocals)) {
ListWrapper.forEach(binder.eventNames, (eventName) => {
this._createEventListener(view, element, binderIdx, eventName, binder.eventLocals);
});
if (isPresent(binder.eventLocals) && isPresent(binder.localEvents)) {
for (var i = 0; i < binder.localEvents.length; i++) {
this._createEventListener(view, element, binderIdx, binder.localEvents[i].name, binder.eventLocals);
}
}
}

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) {