diff --git a/lib/core_dom/directive_injector.dart b/lib/core_dom/directive_injector.dart index 07959541d..0910c05a2 100644 --- a/lib/core_dom/directive_injector.dart +++ b/lib/core_dom/directive_injector.dart @@ -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; } diff --git a/lib/core_dom/event_handler.dart b/lib/core_dom/event_handler.dart index b47fb59b7..bffbe3bcf 100644 --- a/lib/core_dom/event_handler.dart +++ b/lib/core_dom/event_handler.dart @@ -31,7 +31,7 @@ class EventHandler { dom.Node _rootNode; final Expando _expando; final ExceptionHandler _exceptionHandler; - final _listeners = new HashMap(); + final _listeners = new HashMap(); EventHandler(this._rootNode, this._expando, this._exceptionHandler); @@ -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); } @@ -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; } diff --git a/lib/core_dom/ng_element.dart b/lib/core_dom/ng_element.dart index 08de548d2..a58e365af 100644 --- a/lib/core_dom/ng_element.dart +++ b/lib/core_dom/ng_element.dart @@ -7,19 +7,24 @@ class NgElement { final dom.Element node; final Scope _scope; final Animate _animate; + final EventHandler _eventHandler; final _classesToUpdate = new HashMap(); final _attributesToUpdate = new HashMap(); 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; diff --git a/lib/core_dom/view_factory.dart b/lib/core_dom/view_factory.dart index ba66eed75..f546180b3 100644 --- a/lib/core_dom/view_factory.dart +++ b/lib/core_dom/view_factory.dart @@ -203,10 +203,15 @@ class ElementProbe { final DirectiveInjector injector; final Scope scope; List get directives => injector.directives; + final Map> listeners = {}; final bindingExpressions = []; final modelExpressions = []; 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); + } } diff --git a/lib/directive/ng_model_select.dart b/lib/directive/ng_model_select.dart index c44139de0..0c1f506fb 100644 --- a/lib/directive/ng_model_select.dart +++ b/lib/directive/ng_model_select.dart @@ -24,6 +24,7 @@ part of angular.directive; class InputSelect implements AttachAware { final expando = new Expando(); final dom.SelectElement _selectElement; + final NgElement _selectNgElement; final NodeAttrs _attrs; final NgModel _model; final Scope _scope; @@ -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); @@ -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 diff --git a/lib/mock/test_bed.dart b/lib/mock/test_bed.dart index e0cf5064d..600c032ed 100644 --- a/lib/mock/test_bed.dart +++ b/lib/mock/test_bed.dart @@ -13,16 +13,17 @@ class TestBed { final Compiler compiler; final Parser _parser; final Expando expando; + final EventHandler _eventHandler; Element rootElement; List 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)); /** @@ -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(); } diff --git a/test/core_dom/event_handler_spec.dart b/test/core_dom/event_handler_spec.dart index bccf7d4ad..430c5d5e4 100644 --- a/test/core_dom/event_handler_spec.dart +++ b/test/core_dom/event_handler_spec.dart @@ -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', @@ -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( + '''
+
+
'''); + 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( + '''
+
+
'''); + 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( '''
'''); - 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); }); @@ -59,22 +99,21 @@ main() { '''
{{ctrl.description}}
'''); - 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(''); - 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); })); @@ -86,17 +125,16 @@ main() {
'''); - 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); })); }); diff --git a/test/core_dom/ng_element_spec.dart b/test/core_dom/ng_element_spec.dart index 40aa76247..b51789f52 100644 --- a/test/core_dom/ng_element_spec.dart +++ b/test/core_dom/ng_element_spec.dart @@ -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('
'); - var ngElement = new NgElement(element, scope, animate); + var ngElement = new NgElement(element, scope, animate, eventHandler); ngElement..addClass('one')..addClass('two three'); @@ -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('
'); - var ngElement = new NgElement(element, scope, animate); + var ngElement = new NgElement(element, scope, animate, eventHandler); ngElement..removeClass('one') ..removeClass('two') @@ -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('
'); - var ngElement = new NgElement(element, scope, animate); + var ngElement = new NgElement(element, scope, animate, eventHandler); ngElement..addClass('one') ..addClass('one') @@ -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('
'); - var ngElement = new NgElement(element, scope, animate); + var ngElement = new NgElement(element, scope, animate, eventHandler); ngElement.setAttribute('id', 'foo'); ngElement.setAttribute('title', 'bar'); @@ -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('
'); - var ngElement = new NgElement(element, scope, animate); + var ngElement = new NgElement(element, scope, animate, eventHandler); ngElement..removeAttribute('id') ..removeAttribute('title'); @@ -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('
'); - var ngElement = new NgElement(element, scope, animate); + var ngElement = new NgElement(element, scope, animate, eventHandler); ngElement..setAttribute('id', 'foo') ..setAttribute('id', 'foo') diff --git a/test/directive/ng_model_select_spec.dart b/test/directive/ng_model_select_spec.dart index 11d47f344..a36ba9489 100644 --- a/test/directive/ng_model_select_spec.dart +++ b/test/directive/ng_model_select_spec.dart @@ -4,13 +4,50 @@ import '../_specs.dart'; //TODO(misko): re-enabled disabled tests once we have forms. +@Controller(selector: '[value-ctrl]', publishAs: 'ctrl') +class NgValueController { + String seenValue = ""; + String currentValue = "a"; + + void onSelectionChanged() { + seenValue = currentValue; + } +} + main() { describe('input-select', () { describe('ng-value', () { TestBed _; + beforeEachModule((Module module) { + module.bind(NgValueController); + }); beforeEach((TestBed tb) => _ = tb); - it('should retrieve using ng-value', (Application app) { + it('should update model before calling function', (Application app) { + var fooElement = _.compile( + '
' + '' + '
'); + document.body.append(app.element..append(fooElement)); + + var selectElement = fooElement.children.first; + _.rootScope.apply(); + + expect(selectElement).toEqualSelect([['a'], 'b', 'c']); + expect(_.rootScope.context['selectProbe'].injector.get(NgValueController).seenValue).toEqual(""); + + selectElement.querySelectorAll('option')[1].selected = true; + _.triggerEvent(selectElement, 'change'); + + expect(selectElement).toEqualSelect(['a', ['b'], 'c']); + expect(_.rootScope.context['selectProbe'].injector.get(NgValueController).seenValue).toEqual("b"); + }); + + it('should retrieve using ng-value', (Application app) { var selectElement = _.compile( ''); document.body.append(app.element..append(selectElement)); _.rootScope.apply(); - var select = _.rootScope.context['p'].directive(InputSelect); expect(selectElement).toEqualSelect(['', ['x'], 'y']); @@ -432,12 +468,6 @@ main() { describe('select from angular.js', () { var scope, formElement, element; TestBed _; -// Element ngAppElement; -// beforeEachModule((Module module) { -// ngAppElement = new DivElement()..attributes['ng-app'] = ''; -// module..bind(Node, toValue: ngAppElement); -// document.body.append(ngAppElement); -// }); beforeEach((TestBed tb, Scope rootScope) { _ = tb; @@ -461,7 +491,7 @@ main() { it('should compile children of a select without a ngModel, but not create a model for it', () { - compile( + var selectElement = _.compile( '