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

View File

@ -118,9 +118,7 @@ export class ProtoViewFactory {
protoView.bindElementProperty(astWithSource.ast, propertyName); protoView.bindElementProperty(astWithSource.ast, propertyName);
}); });
// events // events
MapWrapper.forEach(renderElementBinder.eventBindings, (astWithSource, eventName) => { protoView.bindEvent(renderElementBinder.eventBindings, -1);
protoView.bindEvent(eventName, astWithSource.ast, -1);
});
// variables // variables
// The view's locals needs to have a full set of variable names at construction time // 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 // 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); protoView.bindDirectiveProperty(i, astWithSource.ast, propertyName, setter);
}); });
// directive events // directive events
MapWrapper.forEach(renderDirectiveMetadata.eventBindings, (astWithSource, eventName) => { protoView.bindEvent(renderDirectiveMetadata.eventBindings, i);
protoView.bindEvent(eventName, astWithSource.ast, i);
});
} }
} }

View File

@ -272,7 +272,7 @@ export class AppView {
var elBinder = this.proto.elementBinders[elementIndex]; var elBinder = this.proto.elementBinders[elementIndex];
if (isBlank(elBinder.hostListeners)) return; if (isBlank(elBinder.hostListeners)) return;
var eventMap = elBinder.hostListeners[eventName]; var eventMap = elBinder.hostListeners[eventName];
if (isBlank(eventName)) return; if (isBlank(eventMap)) return;
MapWrapper.forEach(eventMap, (expr, directiveIndex) => { MapWrapper.forEach(eventMap, (expr, directiveIndex) => {
var context; var context;
if (directiveIndex === -1) { 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 * @param {int} directiveIndex The directive index in the binder or -1 when the event is not bound
* to a directive * 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 elBinder = this.elementBinders[this.elementBinders.length - 1];
var events = elBinder.hostListeners; var events = elBinder.hostListeners;
if (isBlank(events)) { if (isBlank(events)) {
events = StringMapWrapper.create(); events = StringMapWrapper.create();
elBinder.hostListeners = events; elBinder.hostListeners = events;
} }
var event = StringMapWrapper.get(events, eventName); for (var i = 0; i < eventBindings.length; i++) {
if (isBlank(event)) { var eventBinding = eventBindings[i];
event = MapWrapper.create(); var eventName = eventBinding.fullName;
StringMapWrapper.set(events, eventName, event); 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. // addEventListener misses zones so we use element.on.
element.on[event].listen(callback); 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) { void dispatchEvent(EventTarget el, Event evt) {
el.dispatchEvent(evt); el.dispatchEvent(evt);
} }
@ -288,4 +294,13 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
int keyCode = event.keyCode; int keyCode = event.keyCode;
return _keyCodeToKeyMap.containsKey(keyCode) ? _keyCodeToKeyMap[keyCode] : 'Unidentified'; 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) { on(el, evt, listener) {
el.addEventListener(evt, listener, false); 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) { dispatchEvent(el, evt) {
el.dispatchEvent(evt); el.dispatchEvent(evt);
} }
@ -353,4 +359,13 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
} }
return key; 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) { on(el, evt, listener) {
throw _abstract(); throw _abstract();
} }
onAndCancel(el, evt, listener): Function {
throw _abstract();
}
dispatchEvent(el, evt) { dispatchEvent(el, evt) {
throw _abstract(); throw _abstract();
} }
@ -273,4 +276,7 @@ export class DomAdapter {
supportsNativeShadowDOM(): boolean { supportsNativeShadowDOM(): boolean {
throw _abstract(); throw _abstract();
} }
getGlobalEventTarget(target:string) {
throw _abstract();
}
} }

View File

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

View File

@ -86,6 +86,9 @@ export class Parse5DomAdapter extends DomAdapter {
on(el, evt, listener) { on(el, evt, listener) {
//Do nothing, in order to not break forms integration tests //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) { dispatchEvent(el, evt) {
throw _notImplemented('dispatchEvent'); throw _notImplemented('dispatchEvent');
} }

View File

@ -10,6 +10,6 @@ export class MockVmTurnZone extends VmTurnZone {
} }
runOutsideAngular(fn) { 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 * - render compiler is not on the critical path as
* its output will be stored in precompiled templates. * 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 { export class ElementBinder {
index:number; index:number;
parentIndex:number; parentIndex:number;
@ -26,7 +36,7 @@ export class ElementBinder {
// Note: this contains a preprocessed AST // Note: this contains a preprocessed AST
// that replaced the values that should be extracted from the element // that replaced the values that should be extracted from the element
// with a local name // with a local name
eventBindings: Map<string, ASTWithSource>; eventBindings: List<EventBinding>;
textBindings: List<ASTWithSource>; textBindings: List<ASTWithSource>;
readAttributes: Map<string, string>; readAttributes: Map<string, string>;
@ -57,7 +67,7 @@ export class DirectiveBinder {
// Note: this contains a preprocessed AST // Note: this contains a preprocessed AST
// that replaced the values that should be extracted from the element // that replaced the values that should be extracted from the element
// with a local name // with a local name
eventBindings: Map<string, ASTWithSource>; eventBindings: List<EventBinding>;
constructor({ constructor({
directiveIndex, propertyBindings, eventBindings 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 {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter'; import {DOM} from 'angular2/src/dom/dom_adapter';
import {Parser} from 'angular2/change_detection'; import {Parser} from 'angular2/change_detection';
@ -10,7 +10,7 @@ import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control'; import {CompileControl} from './compile_control';
import {DirectiveMetadata} from '../../api'; 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 * 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) { _bindDirectiveEvent(eventName, action, compileElement, directiveBinder) {
var ast = this._parser.parseAction(action, compileElement.elementDescription); 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) { _splitBindConfig(bindConfig:string) {

View File

@ -18,13 +18,15 @@ export class EventManager {
} }
addEventListener(element, eventName: string, handler: Function) { addEventListener(element, eventName: string, handler: Function) {
var shouldSupportBubble = eventName[0] == BUBBLE_SYMBOL; var withoutBubbleSymbol = this._removeBubbleSymbol(eventName);
if (shouldSupportBubble) { var plugin = this._findPluginFor(withoutBubbleSymbol);
eventName = StringWrapper.substring(eventName, 1); plugin.addEventListener(element, withoutBubbleSymbol, handler, withoutBubbleSymbol != eventName);
} }
var plugin = this._findPluginFor(eventName); addGlobalEventListener(target: string, eventName: string, handler: Function): Function {
plugin.addEventListener(element, eventName, handler, shouldSupportBubble); var withoutBubbleSymbol = this._removeBubbleSymbol(eventName);
var plugin = this._findPluginFor(withoutBubbleSymbol);
return plugin.addGlobalEventListener(target, withoutBubbleSymbol, handler, withoutBubbleSymbol != eventName);
} }
getZone(): VmTurnZone { getZone(): VmTurnZone {
@ -41,6 +43,10 @@ export class EventManager {
} }
throw new BaseException(`No event manager plugin found for event ${eventName}`); 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 { export class EventManagerPlugin {
@ -54,8 +60,11 @@ export class EventManagerPlugin {
return false; return false;
} }
addEventListener(element, eventName: string, handler: Function, addEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean) {
shouldSupportBubble: boolean) { throw "not implemented";
}
addGlobalEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean): Function {
throw "not implemented"; throw "not implemented";
} }
} }
@ -69,17 +78,27 @@ export class DomEventsPlugin extends EventManagerPlugin {
return true; return true;
} }
addEventListener(element, eventName: string, handler: Function, addEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean) {
shouldSupportBubble: boolean) { var outsideHandler = this._getOutsideHandler(shouldSupportBubble, element, handler, this.manager._zone);
var outsideHandler = shouldSupportBubble ?
DomEventsPlugin.bubbleCallback(element, handler, this.manager._zone) :
DomEventsPlugin.sameElementCallback(element, handler, this.manager._zone);
this.manager._zone.runOutsideAngular(() => { this.manager._zone.runOutsideAngular(() => {
DOM.on(element, eventName, outsideHandler); 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) { static sameElementCallback(element, handler, zone) {
return (event) => { return (event) => {
if (event.target === element) { 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_SELECTOR = '.ng-binding';
export const NG_BINDING_CLASS = 'ng-binding'; export const NG_BINDING_CLASS = 'ng-binding';
export const EVENT_TARGET_SEPARATOR = ':';
var CAMEL_CASE_REGEXP = RegExpWrapper.create('([A-Z])'); var CAMEL_CASE_REGEXP = RegExpWrapper.create('([A-Z])');
var DASH_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>; textNodeIndices: List<number>;
nestedProtoView: protoViewModule.RenderProtoView; nestedProtoView: protoViewModule.RenderProtoView;
eventLocals: AST; eventLocals: AST;
eventNames: List<string>; localEvents: List<Event>;
globalEvents: List<Event>;
componentId: string; componentId: string;
parentIndex:number; parentIndex:number;
distanceToParent:number; distanceToParent:number;
@ -20,7 +21,8 @@ export class ElementBinder {
nestedProtoView, nestedProtoView,
componentId, componentId,
eventLocals, eventLocals,
eventNames, localEvents,
globalEvents,
parentIndex, parentIndex,
distanceToParent, distanceToParent,
propertySetters propertySetters
@ -30,9 +32,22 @@ export class ElementBinder {
this.nestedProtoView = nestedProtoView; this.nestedProtoView = nestedProtoView;
this.componentId = componentId; this.componentId = componentId;
this.eventLocals = eventLocals; this.eventLocals = eventLocals;
this.eventNames = eventNames; this.localEvents = localEvents;
this.globalEvents = globalEvents;
this.parentIndex = parentIndex; this.parentIndex = parentIndex;
this.distanceToParent = distanceToParent; this.distanceToParent = distanceToParent;
this.propertySetters = propertySetters; 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 {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 {DOM} from 'angular2/src/dom/dom_adapter';
import { import {
@ -8,13 +8,13 @@ import {
import {SetterFn} from 'angular2/src/reflection/types'; import {SetterFn} from 'angular2/src/reflection/types';
import {RenderProtoView} from './proto_view'; import {RenderProtoView} from './proto_view';
import {ElementBinder} from './element_binder'; import {ElementBinder, Event} from './element_binder';
import {setterFactory} from './property_setter_factory'; import {setterFactory} from './property_setter_factory';
import * as api from '../../api'; import * as api from '../../api';
import * as directDomRenderer from '../direct_dom_renderer'; 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 { export class ProtoViewBuilder {
rootElement; rootElement;
@ -56,12 +56,12 @@ export class ProtoViewBuilder {
var apiElementBinders = []; var apiElementBinders = [];
ListWrapper.forEach(this.elements, (ebb) => { ListWrapper.forEach(this.elements, (ebb) => {
var propertySetters = MapWrapper.create(); var propertySetters = MapWrapper.create();
var eventLocalsAstSplitter = new EventLocalsAstSplitter();
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (db) => { var apiDirectiveBinders = ListWrapper.map(ebb.directives, (db) => {
ebb.eventBuilder.merge(db.eventBuilder);
return new api.DirectiveBinder({ return new api.DirectiveBinder({
directiveIndex: db.directiveIndex, directiveIndex: db.directiveIndex,
propertyBindings: db.propertyBindings, propertyBindings: db.propertyBindings,
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(db.eventBindings) eventBindings: db.eventBindings
}); });
}); });
MapWrapper.forEach(ebb.propertySetters, (setter, propertyName) => { MapWrapper.forEach(ebb.propertySetters, (setter, propertyName) => {
@ -75,7 +75,7 @@ export class ProtoViewBuilder {
directives: apiDirectiveBinders, directives: apiDirectiveBinders,
nestedProtoView: nestedProtoView, nestedProtoView: nestedProtoView,
propertyBindings: ebb.propertyBindings, variableBindings: ebb.variableBindings, propertyBindings: ebb.propertyBindings, variableBindings: ebb.variableBindings,
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(ebb.eventBindings), eventBindings: ebb.eventBindings,
textBindings: ebb.textBindings, textBindings: ebb.textBindings,
readAttributes: ebb.readAttributes readAttributes: ebb.readAttributes
})); }));
@ -86,8 +86,9 @@ export class ProtoViewBuilder {
distanceToParent: ebb.distanceToParent, distanceToParent: ebb.distanceToParent,
nestedProtoView: isPresent(nestedProtoView) ? nestedProtoView.render.delegate : null, nestedProtoView: isPresent(nestedProtoView) ? nestedProtoView.render.delegate : null,
componentId: ebb.componentId, componentId: ebb.componentId,
eventLocals: eventLocalsAstSplitter.buildEventLocals(), eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()),
eventNames: eventLocalsAstSplitter.buildEventNames(), localEvents: ebb.eventBuilder.buildLocalEvents(),
globalEvents: ebb.eventBuilder.buildGlobalEvents(),
propertySetters: propertySetters propertySetters: propertySetters
})); }));
}); });
@ -112,7 +113,8 @@ export class ElementBinderBuilder {
nestedProtoView:ProtoViewBuilder; nestedProtoView:ProtoViewBuilder;
propertyBindings: Map<string, ASTWithSource>; propertyBindings: Map<string, ASTWithSource>;
variableBindings: Map<string, string>; variableBindings: Map<string, string>;
eventBindings: Map<string, ASTWithSource>; eventBindings: List<api.EventBinding>;
eventBuilder: EventBuilder;
textBindingIndices: List<number>; textBindingIndices: List<number>;
textBindings: List<ASTWithSource>; textBindings: List<ASTWithSource>;
contentTagSelector:string; contentTagSelector:string;
@ -129,7 +131,8 @@ export class ElementBinderBuilder {
this.nestedProtoView = null; this.nestedProtoView = null;
this.propertyBindings = MapWrapper.create(); this.propertyBindings = MapWrapper.create();
this.variableBindings = MapWrapper.create(); this.variableBindings = MapWrapper.create();
this.eventBindings = MapWrapper.create(); this.eventBindings = ListWrapper.create();
this.eventBuilder = new EventBuilder();
this.textBindings = []; this.textBindings = [];
this.textBindingIndices = []; this.textBindingIndices = [];
this.contentTagSelector = null; this.contentTagSelector = null;
@ -191,8 +194,8 @@ export class ElementBinderBuilder {
} }
} }
bindEvent(name, expression) { bindEvent(name, expression, target = null) {
MapWrapper.set(this.eventBindings, name, expression); ListWrapper.push(this.eventBindings, this.eventBuilder.add(name, expression, target));
} }
bindText(index, expression) { bindText(index, expression) {
@ -212,49 +215,53 @@ export class ElementBinderBuilder {
export class DirectiveBuilder { export class DirectiveBuilder {
directiveIndex:number; directiveIndex:number;
propertyBindings: Map<string, ASTWithSource>; propertyBindings: Map<string, ASTWithSource>;
eventBindings: Map<string, ASTWithSource>; eventBindings: List<api.EventBinding>;
eventBuilder: EventBuilder;
constructor(directiveIndex) { constructor(directiveIndex) {
this.directiveIndex = directiveIndex; this.directiveIndex = directiveIndex;
this.propertyBindings = MapWrapper.create(); this.propertyBindings = MapWrapper.create();
this.eventBindings = MapWrapper.create(); this.eventBindings = ListWrapper.create();
this.eventBuilder = new EventBuilder();
} }
bindProperty(name, expression) { bindProperty(name, expression) {
MapWrapper.set(this.propertyBindings, name, expression); MapWrapper.set(this.propertyBindings, name, expression);
} }
bindEvent(name, expression) { bindEvent(name, expression, target = null) {
MapWrapper.set(this.eventBindings, name, expression); ListWrapper.push(this.eventBindings, this.eventBuilder.add(name, expression, target));
} }
} }
export class EventLocalsAstSplitter extends AstTransformer { export class EventBuilder extends AstTransformer {
locals:List<AST>; locals: List<AST>;
eventNames:List<string>; localEvents: List<Event>;
_implicitReceiver:AST; globalEvents: List<Event>;
_implicitReceiver: AST;
constructor() { constructor() {
super(); super();
this.locals = []; this.locals = [];
this.eventNames = []; this.localEvents = [];
this.globalEvents = [];
this._implicitReceiver = new ImplicitReceiver(); this._implicitReceiver = new ImplicitReceiver();
} }
splitEventAstIntoLocals(eventBindings:Map<string, ASTWithSource>):Map<string, ASTWithSource> { add(name: string, source: ASTWithSource, target: string): api.EventBinding {
if (isPresent(eventBindings)) { // TODO(tbosch): reenable this when we are parsing element properties
var result = MapWrapper.create(); // out of action expressions
MapWrapper.forEach(eventBindings, (astWithSource, eventName) => { // var adjustedAst = astWithSource.ast.visit(this);
// TODO(tbosch): reenable this when we are parsing element properties var adjustedAst = source.ast;
// out of action expressions var fullName = isPresent(target) ? target + EVENT_TARGET_SEPARATOR + name : name;
// var adjustedAst = astWithSource.ast.visit(this); var result = new api.EventBinding(fullName, new ASTWithSource(adjustedAst, source.source, ''));
var adjustedAst = astWithSource.ast; var event = new Event(name, target, fullName);
MapWrapper.set(result, eventName, new ASTWithSource(adjustedAst, astWithSource.source, '')); if (isBlank(target)) {
ListWrapper.push(this.eventNames, eventName); ListWrapper.push(this.localEvents, event);
}); } else {
return result; ListWrapper.push(this.globalEvents, event);
} }
return null; return result;
} }
visitAccessMember(ast:AccessMember) { visitAccessMember(ast:AccessMember) {
@ -277,10 +284,32 @@ export class EventLocalsAstSplitter extends AstTransformer {
} }
buildEventLocals() { buildEventLocals() {
return new LiteralArray(this.locals); return this.locals;
} }
buildEventNames() { buildLocalEvents() {
return this.eventNames; 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 {RenderProtoView} from './proto_view';
import {LightDom} from '../shadow_dom/light_dom'; import {LightDom} from '../shadow_dom/light_dom';
import {Content} from '../shadow_dom/content_tag'; 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'; import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
@ -29,12 +30,14 @@ export class RenderView {
contentTags: List<Content>; contentTags: List<Content>;
lightDoms: List<LightDom>; lightDoms: List<LightDom>;
proto: RenderProtoView; proto: RenderProtoView;
eventManager: EventManager;
_hydrated: boolean; _hydrated: boolean;
_eventDispatcher: any/*EventDispatcher*/; _eventDispatcher: any/*EventDispatcher*/;
_eventHandlerRemovers: List<Function>;
constructor( constructor(
proto:RenderProtoView, rootNodes:List, 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.proto = proto;
this.rootNodes = rootNodes; this.rootNodes = rootNodes;
this.boundTextNodes = boundTextNodes; this.boundTextNodes = boundTextNodes;
@ -42,9 +45,11 @@ export class RenderView {
this.viewContainers = viewContainers; this.viewContainers = viewContainers;
this.contentTags = contentTags; this.contentTags = contentTags;
this.lightDoms = ListWrapper.createFixedSize(boundElements.length); this.lightDoms = ListWrapper.createFixedSize(boundElements.length);
this.eventManager = eventManager;
ListWrapper.fill(this.lightDoms, null); ListWrapper.fill(this.lightDoms, null);
this.componentChildViews = ListWrapper.createFixedSize(boundElements.length); this.componentChildViews = ListWrapper.createFixedSize(boundElements.length);
this._hydrated = false; this._hydrated = false;
this._eventHandlerRemovers = null;
} }
hydrated() { hydrated() {
@ -130,6 +135,26 @@ export class RenderView {
lightDom.redistribute(); 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() { 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._eventDispatcher = null;
this._hydrated = false; this._hydrated = false;
} }

View File

@ -125,7 +125,7 @@ export class ViewFactory {
var view = new viewModule.RenderView( var view = new viewModule.RenderView(
protoView, viewRootNodes, protoView, viewRootNodes,
boundTextNodes, boundElements, viewContainers, contentTags boundTextNodes, boundElements, viewContainers, contentTags, this._eventManager
); );
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) { for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
@ -139,10 +139,10 @@ export class ViewFactory {
} }
// events // events
if (isPresent(binder.eventLocals)) { if (isPresent(binder.eventLocals) && isPresent(binder.localEvents)) {
ListWrapper.forEach(binder.eventNames, (eventName) => { for (var i = 0; i < binder.localEvents.length; i++) {
this._createEventListener(view, element, binderIdx, eventName, binder.eventLocals); 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 {NgElement} from 'angular2/src/core/compiler/ng_element';
import {Directive} from 'angular2/src/core/annotations/annotations'; import {Directive} from 'angular2/src/core/annotations/annotations';
import {BindingPropagationConfig, Parser, Lexer} from 'angular2/change_detection'; import {BindingPropagationConfig, Parser, Lexer} from 'angular2/change_detection';
import {ViewRef, Renderer, EventBinding} from 'angular2/src/render/api';
import {ViewRef, Renderer} from 'angular2/src/render/api';
import {QueryList} from 'angular2/src/core/compiler/query_list'; import {QueryList} from 'angular2/src/core/compiler/query_list';
class DummyDirective extends Directive { class DummyDirective extends Directive {
@ -701,7 +700,9 @@ export function main() {
StringMapWrapper.set(handlers, eventName, eventHandler); StringMapWrapper.set(handlers, eventName, eventHandler);
var pv = new AppProtoView(null, null, null); var pv = new AppProtoView(null, null, null);
pv.bindElement(null, 0, 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()); var view = new AppView(pv, MapWrapper.create());
view.context = new ContextWithHandler(eventHandler); view.context = new ContextWithHandler(eventHandler);

View File

@ -17,7 +17,7 @@ import {
import {TestBed} from 'angular2/src/test_lib/test_bed'; import {TestBed} from 'angular2/src/test_lib/test_bed';
import {DOM} from 'angular2/src/dom/dom_adapter'; 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 {PromiseWrapper} from 'angular2/src/facade/async';
import {Injector, bind} from 'angular2/di'; import {Injector, bind} from 'angular2/di';
@ -541,6 +541,66 @@ export function main() {
async.done(); 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", () => { describe("dynamic components", () => {
@ -894,18 +954,49 @@ class DecoratorListeningEvent {
@Decorator({ @Decorator({
selector: '[listener]', 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 { class DecoratorListeningDomEvent {
eventType: string; eventType: string;
constructor() { constructor() {
this.eventType = ''; this.eventType = '';
} }
onEvent(eventType: string) { onEvent(eventType: string) {
this.eventType = eventType; 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({ @Component({

View File

@ -23,7 +23,8 @@ export function main() {
someDecorator, someDecorator,
someDecoratorIgnoringChildren, someDecoratorIgnoringChildren,
someDecoratorWithProps, someDecoratorWithProps,
someDecoratorWithEvents someDecoratorWithEvents,
someDecoratorWithGlobalEvents
]; ];
parser = new Parser(new Lexer()); parser = new Parser(new Lexer());
}); });
@ -130,8 +131,21 @@ export function main() {
el('<div some-decor-events></div>') el('<div some-decor-events></div>')
); );
var directiveBinding = results[0].directives[0]; var directiveBinding = results[0].directives[0];
expect(MapWrapper.get(directiveBinding.eventBindings, 'click').source) expect(directiveBinding.eventBindings.length).toEqual(1);
.toEqual('doIt()'); 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', () => { describe('viewport directives', () => {
@ -246,3 +260,10 @@ var someDecoratorWithEvents = new DirectiveMetadata({
'click': 'doIt()' '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', () => { it('should detect () syntax', () => {
var results = process(el('<div (click)="b()"></div>')); 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 // "(click[])" is not an expected syntax and is only used to validate the regexp
results = process(el('<div (click[])="b()"></div>')); 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 ()', () => { 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', () => { it('should parse event handlers using () syntax as actions', () => {
var results = process(el('<div (click)="foo=bar"></div>')); 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', () => { it('should detect on- syntax', () => {
var results = process(el('<div on-click="b()"></div>')); 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', () => { it('should parse event handlers using on- syntax as actions', () => {
var results = process(el('<div on-click="foo=bar"></div>')); 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', () => { it('should store bound properties as temporal attributes', () => {

View File

@ -83,6 +83,28 @@ export function main() {
expect(receivedEvent).toBe(dispatchedEvent); 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) { addEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean) {
MapWrapper.set(shouldSupportBubble ? this._bubbleEventHandlers : this._nonBubbleEventHandlers, MapWrapper.set(shouldSupportBubble ? this._bubbleEventHandlers : this._nonBubbleEventHandlers,
eventName, handler); eventName, handler);
return () => {MapWrapper.delete(shouldSupportBubble ? this._bubbleEventHandlers : this._nonBubbleEventHandlers,
eventName)};
} }
} }
@ -117,6 +141,6 @@ class FakeVmTurnZone extends VmTurnZone {
} }
runOutsideAngular(fn) { runOutsideAngular(fn) {
fn(); return fn();
} }
} }

View File

@ -173,6 +173,7 @@ export class FakeEventManagerPlugin extends EventManagerPlugin {
addEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean) { addEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean) {
MapWrapper.set(this._eventHandlers, eventName, handler); 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', () => { it('should attach the view nodes as child of the host element', () => {
var host = el('<div><span>original content</span></div>'); var host = el('<div><span>original content</span></div>');
var nodes = el('<div>view</div>'); var nodes = el('<div>view</div>');
var view = new RenderView(null, [nodes], [], [], [], []); var view = new RenderView(null, [nodes], [], [], [], [], null);
strategy.attachTemplate(host, view); strategy.attachTemplate(host, view);
var firstChild = DOM.firstChild(host); 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', () => { it('should attach the view nodes as child of the host element', () => {
var host = el('<div><span>original content</span></div>'); var host = el('<div><span>original content</span></div>');
var nodes = el('<div>view</div>'); var nodes = el('<div>view</div>');
var view = new RenderView(null, [nodes], [], [], [], []); var view = new RenderView(null, [nodes], [], [], [], [], null);
strategy.attachTemplate(host, view); strategy.attachTemplate(host, view);
var firstChild = DOM.firstChild(host); var firstChild = DOM.firstChild(host);

View File

@ -35,7 +35,7 @@ export function main() {
it('should attach the view nodes to the shadow root', () => { it('should attach the view nodes to the shadow root', () => {
var host = el('<div><span>original content</span></div>'); var host = el('<div><span>original content</span></div>');
var nodes = el('<div>view</div>'); var nodes = el('<div>view</div>');
var view = new RenderView(null, [nodes], [], [], [], []); var view = new RenderView(null, [nodes], [], [], [], [], null);
strategy.attachTemplate(host, view); strategy.attachTemplate(host, view);
var shadowRoot = DOM.getShadowRoot(host); 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 {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 {RenderView} from 'angular2/src/render/dom/view/view';
import {ShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/shadow_dom_strategy'; import {ShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/shadow_dom_strategy';
import {LightDom} from 'angular2/src/render/dom/shadow_dom/light_dom'; 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() { export function main() {
function createView() { function createView() {
var proto = null; var proto = new RenderProtoView({element: el('<div></div>'), isRootView: false, elementBinders: []});
var rootNodes = [el('<div></div>')]; var rootNodes = [el('<div></div>')];
var boundTextNodes = []; var boundTextNodes = [];
var boundElements = [el('<div></div>')]; var boundElements = [el('<div></div>')];
var viewContainers = []; var viewContainers = [];
var contentTags = []; var contentTags = [];
var eventManager = null;
return new RenderView(proto, rootNodes, return new RenderView(proto, rootNodes,
boundTextNodes, boundElements, viewContainers, contentTags); boundTextNodes, boundElements, viewContainers, contentTags, eventManager);
} }
function createShadowDomStrategy(log) { function createShadowDomStrategy(log) {