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

Commit 64a8e6e

Browse files
mvuksanomhevery
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 8f225aa commit 64a8e6e

9 files changed

+179
-70
lines changed

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 = <String, Function>{};
34+
final _listeners = <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 = <String, bool>{};
1213
final _attributesToUpdate = <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
@@ -171,6 +171,11 @@ class ElementProbe {
171171
final Injector injector;
172172
final Scope scope;
173173
final directives = [];
174+
final Map<String, List<Function>> listeners = {};
174175

175176
ElementProbe(this.parent, this.element, this.injector, this.scope);
177+
178+
void addListener(String eventName, fn(Event)) {
179+
listeners.putIfAbsent(eventName, () => []).add(fn);
180+
}
176181
}

lib/directive/ng_model_select.dart

+5-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ part of angular.directive;
2222
class InputSelect implements AttachAware {
2323
final expando = new Expando<OptionValue>();
2424
final dom.SelectElement _selectElement;
25+
final NgElement _selectNgElement;
2526
final NodeAttrs _attrs;
2627
final NgModel _model;
2728
final Scope _scope;
@@ -32,8 +33,8 @@ class InputSelect implements AttachAware {
3233
_SelectMode _mode = new _SelectMode(null, null, null);
3334
bool _dirty = false;
3435

35-
InputSelect(dom.Element this._selectElement, this._attrs, this._model,
36-
this._scope) {
36+
InputSelect(dom.Element this._selectElement, this._attrs, this._model, this._scope,
37+
this._selectNgElement) {
3738
_unknownOption.value = '?';
3839
_nullOption = _selectElement.querySelectorAll('option')
3940
.firstWhere((o) => o.value == '', orElse: () => null);
@@ -53,7 +54,8 @@ class InputSelect implements AttachAware {
5354
_mode.onModelChange(_model.viewValue);
5455
});
5556

56-
_selectElement.onChange.listen((event) => _mode.onViewChange(event));
57+
_selectNgElement.addEventListener('change', _mode.onViewChange);
58+
5759
_model.render = (value) {
5860
// TODO(misko): this hack need to delay the rendering until after domRead
5961
// because the modelChange reads from the DOM. We should be able to render

lib/mock/test_bed.dart

+9-3
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ class TestBed {
1212
final Compiler compiler;
1313
final Parser _parser;
1414
final Expando expando;
15+
final EventHandler _eventHandler;
1516

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

20-
TestBed(this.injector, this.rootScope, this.compiler, this._parser, this.expando);
21+
TestBed(this.injector, this.rootScope, this.compiler, this._parser, this._eventHandler,
22+
this.expando);
2123

2224

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

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',
@@ -41,14 +58,37 @@ main() {
4158
expect(_.getScope(e).context['ctrl'].invoked).toEqual(true);
4259
}));
4360

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

51-
_.triggerEvent(e.querySelector('[on-my-new-event]'), 'myNewEvent');
91+
_.triggerEvent(e.querySelector('[on-my-new-event]'), 'myNewEvent', 'CustomEvent', true);
5292
var fooScope = _.getScope(e);
5393
expect(fooScope.context['ctrl'].invoked).toEqual(true);
5494
}));
@@ -58,22 +98,21 @@ main() {
5898
'''<div foo>
5999
<div on-abc='ctrl.description="new description";'>{{ctrl.description}}</div>
60100
</div>''');
61-
document.body.append(app.element..append(e));
62-
var el = document.querySelector('[on-abc]');
63-
el.dispatchEvent(new Event('abc'));
101+
102+
var el = e.querySelector('[on-abc]');
103+
_.triggerEvent(el, 'abc', 'CustomEvent', true);
64104
_.rootScope.apply();
65105
expect(el.text).toEqual("new description");
66106
}));
67107

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

72111
microLeap();
73112

74113
var shadowRoot = e.shadowRoot;
75114
var span = shadowRoot.querySelector('span');
76-
span.dispatchEvent(new CustomEvent('abc'));
115+
_.triggerEvent(span, 'abc', 'CustomEvent', true);
77116
var ctrl = _.rootScope.context['ctrl'];
78117
expect(ctrl.invoked).toEqual(true);
79118
}));
@@ -85,16 +124,15 @@ main() {
85124
<div on-abc="ctrl.invoked=true;"></div>
86125
</bar>
87126
</div>''');
88-
document.body.append(app.element..append(e));
89127

90128
microLeap();
91129

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

97-
var fooScope = _.getScope(document.querySelector('[foo]'));
135+
var fooScope = _.getScope(e);
98136
expect(fooScope.context['ctrl'].invoked).toEqual(true);
99137
})));
100138
});

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)