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

Commit

Permalink
feat(NgElement): NgElement uses EventHandler
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mvuksano authored and chirayuk committed Aug 13, 2014
1 parent aa8b6e3 commit bb954b6
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 73 deletions.
2 changes: 1 addition & 1 deletion lib/core_dom/directive_injector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ class DirectiveInjector implements DirectiveBinder {

NgElement get ngElement {
if (_ngElement == null) {
_ngElement = new NgElement(_node, scope, _animate);
_ngElement = new NgElement(_node, scope, _animate, _eventHandler);
}
return _ngElement;
}
Expand Down
49 changes: 36 additions & 13 deletions lib/core_dom/event_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class EventHandler {
dom.Node _rootNode;
final Expando _expando;
final ExceptionHandler _exceptionHandler;
final _listeners = new HashMap<String, Function>();
final _listeners = new HashMap<String, async.StreamSubscription>();

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

Expand All @@ -41,22 +41,40 @@ class EventHandler {
*/
void register(String eventName) {
_listeners.putIfAbsent(eventName, () {
dom.EventListener eventListener = this._eventListener;
_rootNode.on[eventName].listen(eventListener);
return eventListener;
return _rootNode.on[eventName].listen(_eventListener);
});
}

void _eventListener(dom.Event event) {
dom.Node element = event.target;
void registerCallback(dom.Element element, String eventName, EventFunction callbackFn) {
ElementProbe probe = _expando[element];
probe.addListener(eventName, callbackFn);
register(eventName);
}

async.Future releaseListeners() {
return _listeners.forEach((_, async.StreamSubscription subscription) => subscription.cancel());
}

void walkDomTreeAndExecute(dom.Node element, dom.Event event) {
while (element != null && element != _rootNode) {
var expression;
if (element is dom.Element)
var expression, probe;
if (element is dom.Element) {
expression = (element as dom.Element).attributes[eventNameToAttrName(event.type)];
if (expression != null) {
probe = _getProbe(element);
}
if (probe != null && probe.listeners[event.type] != null) {
probe.listeners[event.type].forEach((fn) {
try {
fn(event);
} catch (e, s) {
_exceptionHandler(e, s);
}
});
}
if (probe != null && expression != null) {
try {
var scope = _getScope(element);
if (scope != null) scope.eval(expression);
Scope scope = probe.scope;
if (scope != null) scope.eval(expression, {r'$event': event});
} catch (e, s) {
_exceptionHandler(e, s);
}
Expand All @@ -65,12 +83,17 @@ class EventHandler {
}
}

Scope _getScope(dom.Node element) {
void _eventListener(dom.Event event) {
var element = event.target;
walkDomTreeAndExecute(element, event);
}

ElementProbe _getProbe(dom.Node element) {
// var topElement = (rootNode is dom.ShadowRoot) ? rootNode.parentNode : rootNode;
while (element != _rootNode.parentNode) {
ElementProbe probe = _expando[element];
if (probe != null) {
return probe.scope;
return probe;
}
element = element.parentNode;
}
Expand Down
7 changes: 6 additions & 1 deletion lib/core_dom/ng_element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ class NgElement {
final dom.Element node;
final Scope _scope;
final Animate _animate;
final EventHandler _eventHandler;

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

bool _writeScheduled = false;

NgElement(this.node, this._scope, this._animate);
NgElement(this.node, this._scope, this._animate, this._eventHandler);

void addClass(String className) {
_scheduleDomWrite();
_classesToUpdate[className] = true;
}

void addEventListener(String eventName, callback(Event)) {
_eventHandler.registerCallback(node, eventName, callback);
}

void removeClass(String className) {
_scheduleDomWrite();
_classesToUpdate[className] = false;
Expand Down
5 changes: 5 additions & 0 deletions lib/core_dom/view_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,15 @@ class ElementProbe {
final DirectiveInjector injector;
final Scope scope;
List get directives => injector.directives;
final Map<String, List<Function>> listeners = {};
final bindingExpressions = <String>[];
final modelExpressions = <String>[];

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

dynamic directive(Type type) => injector.get(type);

void addListener(String eventName, fn(Event)) {
listeners.putIfAbsent(eventName, () => []).add(fn);
}
}
8 changes: 5 additions & 3 deletions lib/directive/ng_model_select.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ part of angular.directive;
class InputSelect implements AttachAware {
final expando = new Expando<OptionValue>();
final dom.SelectElement _selectElement;
final NgElement _selectNgElement;
final NodeAttrs _attrs;
final NgModel _model;
final Scope _scope;
Expand All @@ -34,8 +35,8 @@ class InputSelect implements AttachAware {
_SelectMode _mode = new _SelectMode(null, null, null);
bool _dirty = false;

InputSelect(dom.Element this._selectElement, this._attrs, this._model,
this._scope) {
InputSelect(dom.Element this._selectElement, this._attrs, this._model, this._scope,
this._selectNgElement) {
_unknownOption.value = '?';
_nullOption = _selectElement.querySelectorAll('option')
.firstWhere((o) => o.value == '', orElse: () => null);
Expand All @@ -57,7 +58,8 @@ class InputSelect implements AttachAware {
});
});

_selectElement.onChange.listen((event) => _mode.onViewChange(event));
_selectNgElement.addEventListener('change', _mode.onViewChange);

_model.render = (value) {
// TODO(misko): this hack need to delay the rendering until after domRead
// because the modelChange reads from the DOM. We should be able to render
Expand Down
15 changes: 10 additions & 5 deletions lib/mock/test_bed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ class TestBed {
final Compiler compiler;
final Parser _parser;
final Expando expando;
final EventHandler _eventHandler;

Element rootElement;
List<Node> rootElements;
View rootView;

TestBed(this.injector, this.directiveInjector, this.rootScope, this.compiler, this._parser, this.expando);

TestBed(this.injector, this.directiveInjector, this.rootScope, this.compiler,
this._parser, this.expando, this._eventHandler);
TestBed.fromInjector(Injector i) :
this(i, i.get(DirectiveInjector), i.get(RootScope), i.get(Compiler),
i.get(Parser), i.get(Expando));
i.get(Parser), i.get(Expando), i.get(EventHandler));


/**
Expand Down Expand Up @@ -75,8 +76,12 @@ class TestBed {
* Trigger a specific DOM element on a given node to test directives
* which listen to events.
*/
triggerEvent(element, name, [type='MouseEvent']) {
element.dispatchEvent(new Event.eventType(type, name));
triggerEvent(element, name, [String type='MouseEvent', emulateBubbling=false]) {
if (emulateBubbling == true) {
_eventHandler.walkDomTreeAndExecute(element, new Event.eventType(type, name));
} else {
element.dispatchEvent(new Event.eventType(type, name));
}
// Since we are manually triggering event we need to simulate apply();
rootScope.apply();
}
Expand Down
60 changes: 49 additions & 11 deletions test/core_dom/event_handler_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ import '../_specs.dart';
class FooController {
var description = "desc";
var invoked = false;
var anotherInvoked = false;
EventHandler eventHandler;
NgElement element;

FooController(this.element) {
element.addEventListener('cux', onCux);
element.addEventListener('cux', onAnotherCux);
}

void onCux(Event e) {
invoked = true;
}

void onAnotherCux(Event e) {
anotherInvoked = true;
}

}

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

it('should allow registration using method', (TestBed _, Application app) {
var e = _.compile(
'''<div foo>
<div baz></div>
</div>''');
document.body.append(app.element..append(e));

_.triggerEvent(e.querySelector('[baz]'), 'cux');
expect(_.getScope(e).context['ctrl'].invoked).toEqual(true);
});

it('should allow registration of multiple event handlers using method',
(TestBed _, Application app) {
var e = _.compile(
'''<div foo>
<div baz></div>
</div>''');
document.body.append(app.element..append(e));

_.triggerEvent(e.querySelector('[baz]'), 'cux');
expect(_.getScope(e).context['ctrl'].invoked).toEqual(true);
expect(_.getScope(e).context['ctrl'].anotherInvoked).toEqual(true);
});

it('shoud register and handle event with long name', (TestBed _, Application app) {
var e = _.compile(
'''<div foo>
<div on-my-new-event="ctrl.invoked=true"></div>
</div>''');
document.body.append(app.element..append(e));

_.triggerEvent(e.querySelector('[on-my-new-event]'), 'myNewEvent');
_.triggerEvent(e.querySelector('[on-my-new-event]'), 'myNewEvent', 'CustomEvent', true);
var fooScope = _.getScope(e);
expect(fooScope.context['ctrl'].invoked).toEqual(true);
});
Expand All @@ -59,22 +99,21 @@ main() {
'''<div foo>
<div on-abc='ctrl.description="new description"'>{{ctrl.description}}</div>
</div>''');
document.body.append(app.element..append(e));
var el = document.querySelector('[on-abc]');
el.dispatchEvent(new Event('abc'));

var el = e.querySelector('[on-abc]');
_.triggerEvent(el, 'abc', 'CustomEvent', true);
_.rootScope.apply();
expect(el.text).toEqual("new description");
});

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

microLeap();

var shadowRoot = e.shadowRoot;
var span = shadowRoot.querySelector('span');
span.dispatchEvent(new CustomEvent('abc'));
_.triggerEvent(span, 'abc', 'CustomEvent', true);
BarComponent ctrl = _.rootScope.context['barComponent'];
expect(ctrl.invoked).toEqual(true);
}));
Expand All @@ -86,17 +125,16 @@ main() {
<div on-abc="ctrl.invoked=true;"></div>
</bar>
</div>''');
document.body.append(app.element..append(e));

microLeap();

document.querySelector('[on-abc]').dispatchEvent(new Event('abc'));
var shadowRoot = document.querySelector('bar').shadowRoot;
_.triggerEvent(e.querySelector('[on-abc]'), 'abc', 'CustomEvent', true);
var shadowRoot = e.querySelector('bar').shadowRoot;
var shadowRootScope = _.getScope(shadowRoot);
BarComponent ctrl = shadowRootScope.context['ctrl'];
expect(ctrl.invoked).toEqual(false);

var fooScope = _.getScope(document.querySelector('[foo]'));
var fooScope = _.getScope(e);
expect(fooScope.context['ctrl'].invoked).toEqual(true);
}));
});
Expand Down
24 changes: 12 additions & 12 deletions test/core_dom/ng_element_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ void main() {

describe('classes', () {
it('should add classes to the element on domWrite',
(TestBed _, Animate animate) {
(TestBed _, Animate animate, EventHandler eventHandler) {

var scope = _.rootScope;
var element = e('<div></div>');
var ngElement = new NgElement(element, scope, animate);
var ngElement = new NgElement(element, scope, animate, eventHandler);

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

Expand All @@ -27,11 +27,11 @@ void main() {
});

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

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

ngElement..removeClass('one')
..removeClass('two')
Expand All @@ -50,11 +50,11 @@ void main() {
});

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

var scope = _.rootScope;
var element = e('<div></div>');
var ngElement = new NgElement(element, scope, animate);
var ngElement = new NgElement(element, scope, animate, eventHandler);

ngElement..addClass('one')
..addClass('one')
Expand All @@ -79,11 +79,11 @@ void main() {

describe('attributes', () {
it('should set attributes on domWrite to the element',
(TestBed _, Animate animate) {
(TestBed _, Animate animate, EventHandler eventHandler) {

var scope = _.rootScope;
var element = e('<div></div>');
var ngElement = new NgElement(element, scope, animate);
var ngElement = new NgElement(element, scope, animate, eventHandler);

ngElement.setAttribute('id', 'foo');
ngElement.setAttribute('title', 'bar');
Expand All @@ -99,11 +99,11 @@ void main() {
});

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

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

ngElement..removeAttribute('id')
..removeAttribute('title');
Expand All @@ -118,11 +118,11 @@ void main() {
});

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

var scope = _.rootScope;
var element = e('<div></div>');
var ngElement = new NgElement(element, scope, animate);
var ngElement = new NgElement(element, scope, animate, eventHandler);

ngElement..setAttribute('id', 'foo')
..setAttribute('id', 'foo')
Expand Down
Loading

0 comments on commit bb954b6

Please sign in to comment.