Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit bb954b6

Browse files
mvuksanochirayuk
authored andcommitted
feat(NgElement): NgElement uses EventHandler
NgElement uses EventHandler to register event listeners. Event Handler exposes API to register callbacks, release them as well as a method (walkDomTreeAndExecute) that is used (by TestBed.triggerEvent) to emulate even bubbling when DOM tree is not attached to document.body. ElementProbe is also extended to allow attaching additional listeners. This can be used by different components to programatically attach event handlers (see InputSelectElement and it's change event handler). Closes #399
1 parent aa8b6e3 commit bb954b6

10 files changed

+181
-73
lines changed

lib/core_dom/directive_injector.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ class DirectiveInjector implements DirectiveBinder {
337337

338338
NgElement get ngElement {
339339
if (_ngElement == null) {
340-
_ngElement = new NgElement(_node, scope, _animate);
340+
_ngElement = new NgElement(_node, scope, _animate, _eventHandler);
341341
}
342342
return _ngElement;
343343
}

lib/core_dom/event_handler.dart

+36-13
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class EventHandler {
3131
dom.Node _rootNode;
3232
final Expando _expando;
3333
final ExceptionHandler _exceptionHandler;
34-
final _listeners = new HashMap<String, Function>();
34+
final _listeners = new HashMap<String, async.StreamSubscription>();
3535

3636
EventHandler(this._rootNode, this._expando, this._exceptionHandler);
3737

@@ -41,22 +41,40 @@ class EventHandler {
4141
*/
4242
void register(String eventName) {
4343
_listeners.putIfAbsent(eventName, () {
44-
dom.EventListener eventListener = this._eventListener;
45-
_rootNode.on[eventName].listen(eventListener);
46-
return eventListener;
44+
return _rootNode.on[eventName].listen(_eventListener);
4745
});
4846
}
4947

50-
void _eventListener(dom.Event event) {
51-
dom.Node element = event.target;
48+
void registerCallback(dom.Element element, String eventName, EventFunction callbackFn) {
49+
ElementProbe probe = _expando[element];
50+
probe.addListener(eventName, callbackFn);
51+
register(eventName);
52+
}
53+
54+
async.Future releaseListeners() {
55+
return _listeners.forEach((_, async.StreamSubscription subscription) => subscription.cancel());
56+
}
57+
58+
void walkDomTreeAndExecute(dom.Node element, dom.Event event) {
5259
while (element != null && element != _rootNode) {
53-
var expression;
54-
if (element is dom.Element)
60+
var expression, probe;
61+
if (element is dom.Element) {
5562
expression = (element as dom.Element).attributes[eventNameToAttrName(event.type)];
56-
if (expression != null) {
63+
probe = _getProbe(element);
64+
}
65+
if (probe != null && probe.listeners[event.type] != null) {
66+
probe.listeners[event.type].forEach((fn) {
67+
try {
68+
fn(event);
69+
} catch (e, s) {
70+
_exceptionHandler(e, s);
71+
}
72+
});
73+
}
74+
if (probe != null && expression != null) {
5775
try {
58-
var scope = _getScope(element);
59-
if (scope != null) scope.eval(expression);
76+
Scope scope = probe.scope;
77+
if (scope != null) scope.eval(expression, {r'$event': event});
6078
} catch (e, s) {
6179
_exceptionHandler(e, s);
6280
}
@@ -65,12 +83,17 @@ class EventHandler {
6583
}
6684
}
6785

68-
Scope _getScope(dom.Node element) {
86+
void _eventListener(dom.Event event) {
87+
var element = event.target;
88+
walkDomTreeAndExecute(element, event);
89+
}
90+
91+
ElementProbe _getProbe(dom.Node element) {
6992
// var topElement = (rootNode is dom.ShadowRoot) ? rootNode.parentNode : rootNode;
7093
while (element != _rootNode.parentNode) {
7194
ElementProbe probe = _expando[element];
7295
if (probe != null) {
73-
return probe.scope;
96+
return probe;
7497
}
7598
element = element.parentNode;
7699
}

lib/core_dom/ng_element.dart

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,24 @@ class NgElement {
77
final dom.Element node;
88
final Scope _scope;
99
final Animate _animate;
10+
final EventHandler _eventHandler;
1011

1112
final _classesToUpdate = new HashMap<String, bool>();
1213
final _attributesToUpdate = new HashMap<String, dynamic>();
1314

1415
bool _writeScheduled = false;
1516

16-
NgElement(this.node, this._scope, this._animate);
17+
NgElement(this.node, this._scope, this._animate, this._eventHandler);
1718

1819
void addClass(String className) {
1920
_scheduleDomWrite();
2021
_classesToUpdate[className] = true;
2122
}
2223

24+
void addEventListener(String eventName, callback(Event)) {
25+
_eventHandler.registerCallback(node, eventName, callback);
26+
}
27+
2328
void removeClass(String className) {
2429
_scheduleDomWrite();
2530
_classesToUpdate[className] = false;

lib/core_dom/view_factory.dart

+5
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,15 @@ class ElementProbe {
203203
final DirectiveInjector injector;
204204
final Scope scope;
205205
List get directives => injector.directives;
206+
final Map<String, List<Function>> listeners = {};
206207
final bindingExpressions = <String>[];
207208
final modelExpressions = <String>[];
208209

209210
ElementProbe(this.parent, this.element, this.injector, this.scope);
210211

211212
dynamic directive(Type type) => injector.get(type);
213+
214+
void addListener(String eventName, fn(Event)) {
215+
listeners.putIfAbsent(eventName, () => []).add(fn);
216+
}
212217
}

lib/directive/ng_model_select.dart

+5-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ part of angular.directive;
2424
class InputSelect implements AttachAware {
2525
final expando = new Expando<OptionValue>();
2626
final dom.SelectElement _selectElement;
27+
final NgElement _selectNgElement;
2728
final NodeAttrs _attrs;
2829
final NgModel _model;
2930
final Scope _scope;
@@ -34,8 +35,8 @@ class InputSelect implements AttachAware {
3435
_SelectMode _mode = new _SelectMode(null, null, null);
3536
bool _dirty = false;
3637

37-
InputSelect(dom.Element this._selectElement, this._attrs, this._model,
38-
this._scope) {
38+
InputSelect(dom.Element this._selectElement, this._attrs, this._model, this._scope,
39+
this._selectNgElement) {
3940
_unknownOption.value = '?';
4041
_nullOption = _selectElement.querySelectorAll('option')
4142
.firstWhere((o) => o.value == '', orElse: () => null);
@@ -57,7 +58,8 @@ class InputSelect implements AttachAware {
5758
});
5859
});
5960

60-
_selectElement.onChange.listen((event) => _mode.onViewChange(event));
61+
_selectNgElement.addEventListener('change', _mode.onViewChange);
62+
6163
_model.render = (value) {
6264
// TODO(misko): this hack need to delay the rendering until after domRead
6365
// because the modelChange reads from the DOM. We should be able to render

lib/mock/test_bed.dart

+10-5
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@ class TestBed {
1313
final Compiler compiler;
1414
final Parser _parser;
1515
final Expando expando;
16+
final EventHandler _eventHandler;
1617

1718
Element rootElement;
1819
List<Node> rootElements;
1920
View rootView;
2021

21-
TestBed(this.injector, this.directiveInjector, this.rootScope, this.compiler, this._parser, this.expando);
22-
22+
TestBed(this.injector, this.directiveInjector, this.rootScope, this.compiler,
23+
this._parser, this.expando, this._eventHandler);
2324
TestBed.fromInjector(Injector i) :
2425
this(i, i.get(DirectiveInjector), i.get(RootScope), i.get(Compiler),
25-
i.get(Parser), i.get(Expando));
26+
i.get(Parser), i.get(Expando), i.get(EventHandler));
2627

2728

2829
/**
@@ -75,8 +76,12 @@ class TestBed {
7576
* Trigger a specific DOM element on a given node to test directives
7677
* which listen to events.
7778
*/
78-
triggerEvent(element, name, [type='MouseEvent']) {
79-
element.dispatchEvent(new Event.eventType(type, name));
79+
triggerEvent(element, name, [String type='MouseEvent', emulateBubbling=false]) {
80+
if (emulateBubbling == true) {
81+
_eventHandler.walkDomTreeAndExecute(element, new Event.eventType(type, name));
82+
} else {
83+
element.dispatchEvent(new Event.eventType(type, name));
84+
}
8085
// Since we are manually triggering event we need to simulate apply();
8186
rootScope.apply();
8287
}

test/core_dom/event_handler_spec.dart

+49-11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ import '../_specs.dart';
66
class FooController {
77
var description = "desc";
88
var invoked = false;
9+
var anotherInvoked = false;
10+
EventHandler eventHandler;
11+
NgElement element;
12+
13+
FooController(this.element) {
14+
element.addEventListener('cux', onCux);
15+
element.addEventListener('cux', onAnotherCux);
16+
}
17+
18+
void onCux(Event e) {
19+
invoked = true;
20+
}
21+
22+
void onAnotherCux(Event e) {
23+
anotherInvoked = true;
24+
}
25+
926
}
1027

1128
@Component(selector: 'bar',
@@ -42,14 +59,37 @@ main() {
4259
expect(_.getScope(e).context['ctrl'].invoked).toEqual(true);
4360
});
4461

62+
it('should allow registration using method', (TestBed _, Application app) {
63+
var e = _.compile(
64+
'''<div foo>
65+
<div baz></div>
66+
</div>''');
67+
document.body.append(app.element..append(e));
68+
69+
_.triggerEvent(e.querySelector('[baz]'), 'cux');
70+
expect(_.getScope(e).context['ctrl'].invoked).toEqual(true);
71+
});
72+
73+
it('should allow registration of multiple event handlers using method',
74+
(TestBed _, Application app) {
75+
var e = _.compile(
76+
'''<div foo>
77+
<div baz></div>
78+
</div>''');
79+
document.body.append(app.element..append(e));
80+
81+
_.triggerEvent(e.querySelector('[baz]'), 'cux');
82+
expect(_.getScope(e).context['ctrl'].invoked).toEqual(true);
83+
expect(_.getScope(e).context['ctrl'].anotherInvoked).toEqual(true);
84+
});
85+
4586
it('shoud register and handle event with long name', (TestBed _, Application app) {
4687
var e = _.compile(
4788
'''<div foo>
4889
<div on-my-new-event="ctrl.invoked=true"></div>
4990
</div>''');
50-
document.body.append(app.element..append(e));
5191

52-
_.triggerEvent(e.querySelector('[on-my-new-event]'), 'myNewEvent');
92+
_.triggerEvent(e.querySelector('[on-my-new-event]'), 'myNewEvent', 'CustomEvent', true);
5393
var fooScope = _.getScope(e);
5494
expect(fooScope.context['ctrl'].invoked).toEqual(true);
5595
});
@@ -59,22 +99,21 @@ main() {
5999
'''<div foo>
60100
<div on-abc='ctrl.description="new description"'>{{ctrl.description}}</div>
61101
</div>''');
62-
document.body.append(app.element..append(e));
63-
var el = document.querySelector('[on-abc]');
64-
el.dispatchEvent(new Event('abc'));
102+
103+
var el = e.querySelector('[on-abc]');
104+
_.triggerEvent(el, 'abc', 'CustomEvent', true);
65105
_.rootScope.apply();
66106
expect(el.text).toEqual("new description");
67107
});
68108

69109
it('should register event when shadow dom is used', async((TestBed _, Application app) {
70110
var e = _.compile('<bar></bar>');
71-
document.body.append(app.element..append(e));
72111

73112
microLeap();
74113

75114
var shadowRoot = e.shadowRoot;
76115
var span = shadowRoot.querySelector('span');
77-
span.dispatchEvent(new CustomEvent('abc'));
116+
_.triggerEvent(span, 'abc', 'CustomEvent', true);
78117
BarComponent ctrl = _.rootScope.context['barComponent'];
79118
expect(ctrl.invoked).toEqual(true);
80119
}));
@@ -86,17 +125,16 @@ main() {
86125
<div on-abc="ctrl.invoked=true;"></div>
87126
</bar>
88127
</div>''');
89-
document.body.append(app.element..append(e));
90128

91129
microLeap();
92130

93-
document.querySelector('[on-abc]').dispatchEvent(new Event('abc'));
94-
var shadowRoot = document.querySelector('bar').shadowRoot;
131+
_.triggerEvent(e.querySelector('[on-abc]'), 'abc', 'CustomEvent', true);
132+
var shadowRoot = e.querySelector('bar').shadowRoot;
95133
var shadowRootScope = _.getScope(shadowRoot);
96134
BarComponent ctrl = shadowRootScope.context['ctrl'];
97135
expect(ctrl.invoked).toEqual(false);
98136

99-
var fooScope = _.getScope(document.querySelector('[foo]'));
137+
var fooScope = _.getScope(e);
100138
expect(fooScope.context['ctrl'].invoked).toEqual(true);
101139
}));
102140
});

test/core_dom/ng_element_spec.dart

+12-12
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ void main() {
77

88
describe('classes', () {
99
it('should add classes to the element on domWrite',
10-
(TestBed _, Animate animate) {
10+
(TestBed _, Animate animate, EventHandler eventHandler) {
1111

1212
var scope = _.rootScope;
1313
var element = e('<div></div>');
14-
var ngElement = new NgElement(element, scope, animate);
14+
var ngElement = new NgElement(element, scope, animate, eventHandler);
1515

1616
ngElement..addClass('one')..addClass('two three');
1717

@@ -27,11 +27,11 @@ void main() {
2727
});
2828

2929
it('should remove classes from the element on domWrite',
30-
(TestBed _, Animate animate) {
30+
(TestBed _, Animate animate, EventHandler eventHandler) {
3131

3232
var scope = _.rootScope;
3333
var element = e('<div class="one two three four"></div>');
34-
var ngElement = new NgElement(element, scope, animate);
34+
var ngElement = new NgElement(element, scope, animate, eventHandler);
3535

3636
ngElement..removeClass('one')
3737
..removeClass('two')
@@ -50,11 +50,11 @@ void main() {
5050
});
5151

5252
it('should always apply the last dom operation on the given className',
53-
(TestBed _, Animate animate) {
53+
(TestBed _, Animate animate, EventHandler eventHandler) {
5454

5555
var scope = _.rootScope;
5656
var element = e('<div></div>');
57-
var ngElement = new NgElement(element, scope, animate);
57+
var ngElement = new NgElement(element, scope, animate, eventHandler);
5858

5959
ngElement..addClass('one')
6060
..addClass('one')
@@ -79,11 +79,11 @@ void main() {
7979

8080
describe('attributes', () {
8181
it('should set attributes on domWrite to the element',
82-
(TestBed _, Animate animate) {
82+
(TestBed _, Animate animate, EventHandler eventHandler) {
8383

8484
var scope = _.rootScope;
8585
var element = e('<div></div>');
86-
var ngElement = new NgElement(element, scope, animate);
86+
var ngElement = new NgElement(element, scope, animate, eventHandler);
8787

8888
ngElement.setAttribute('id', 'foo');
8989
ngElement.setAttribute('title', 'bar');
@@ -99,11 +99,11 @@ void main() {
9999
});
100100

101101
it('should remove attributes from the element on domWrite ',
102-
(TestBed _, Animate animate) {
102+
(TestBed _, Animate animate, EventHandler eventHandler) {
103103

104104
var scope = _.rootScope;
105105
var element = e('<div id="foo" title="bar"></div>');
106-
var ngElement = new NgElement(element, scope, animate);
106+
var ngElement = new NgElement(element, scope, animate, eventHandler);
107107

108108
ngElement..removeAttribute('id')
109109
..removeAttribute('title');
@@ -118,11 +118,11 @@ void main() {
118118
});
119119

120120
it('should always apply the last operation on the attribute',
121-
(TestBed _, Animate animate) {
121+
(TestBed _, Animate animate, EventHandler eventHandler) {
122122

123123
var scope = _.rootScope;
124124
var element = e('<div></div>');
125-
var ngElement = new NgElement(element, scope, animate);
125+
var ngElement = new NgElement(element, scope, animate, eventHandler);
126126

127127
ngElement..setAttribute('id', 'foo')
128128
..setAttribute('id', 'foo')

0 commit comments

Comments
 (0)