Navigation Menu

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

Commit

Permalink
feat(EventHandler) Add support for on-* style events
Browse files Browse the repository at this point in the history
Fixes #649
Closes #729
  • Loading branch information
mvuksano authored and mhevery committed Mar 15, 2014
1 parent 9b480da commit c28e6a0
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 18 deletions.
3 changes: 2 additions & 1 deletion lib/bootstrap.dart
Expand Up @@ -86,7 +86,8 @@ Injector ngBootstrap({
NgZone zone = new NgZone();
ngModules.add(new Module()
..value(NgZone, zone)
..value(NgApp, new NgApp(element)));
..value(NgApp, new NgApp(element))
..factory(dom.Node, (i) => i.get(NgApp).root));

return zone.run(() {
var rootElements = [element];
Expand Down
16 changes: 9 additions & 7 deletions lib/core_dom/element_binder.dart
Expand Up @@ -23,6 +23,7 @@ class ElementBinder {
Parser _parser;
Profiler _perf;
Expando _expando;
Map<String, String> onEvents = <String, String>{};

// Member fields
List<DirectiveRef> decorators = [];
Expand Down Expand Up @@ -74,9 +75,7 @@ class ElementBinder {
return childMode == NgAnnotation.COMPILE_CHILDREN;
}

ElementBinder get templateBinder {
return new ElementBinder.forTransclusion(this);
}
ElementBinder get templateBinder => new ElementBinder.forTransclusion(this);

List<DirectiveRef> get _usableDirectiveRefs {
if (template != null) {
Expand All @@ -88,9 +87,8 @@ class ElementBinder {
return decorators;
}

bool get hasDirectives {
return (_usableDirectiveRefs != null && _usableDirectiveRefs.length != 0);
}
bool get hasDirectivesOrEvents
=> _usableDirectiveRefs.isNotEmpty || onEvents.isNotEmpty;

// DI visibility callback allowing node-local visibility.

Expand Down Expand Up @@ -122,7 +120,7 @@ class ElementBinder {

var directiveRefs = _usableDirectiveRefs;
try {
if (directiveRefs == null || directiveRefs.length == 0) return parentInjector;
if (!hasDirectivesOrEvents) return parentInjector;
var nodeModule = new Module();
var viewPortFactory = (_) => null;
var viewFactory = (_) => null;
Expand Down Expand Up @@ -269,6 +267,10 @@ class ElementBinder {
assert(_perf.stopTimer(linkTimer) != false);
}
});

onEvents.forEach((event, value) {
view.registerEvent(EventHandler.attrNameToEventName(event));
});
return nodeInjector;
}

Expand Down
74 changes: 74 additions & 0 deletions lib/core_dom/event_handler.dart
@@ -0,0 +1,74 @@
part of angular.core.dom;

typedef void EventFunction(event);

@NgInjectableService()
class EventHandler {
dom.Node rootNode;
final Expando expando;
final ExceptionHandler exceptionHandler;
final Map<String, Function> listeners = <String, Function>{};

EventHandler(this.rootNode, this.expando, this.exceptionHandler);

void register(String eventName) {
listeners.putIfAbsent(eventName, () {
dom.EventListener eventListener = this.eventListener;
rootNode.on[eventName].listen(eventListener);
return eventListener;
});
}

eventListener(dom.Event event) {
dom.Node element = event.target;
while (element != null && element != rootNode) {
var expression;
if (element is dom.Element)
expression = (element as dom.Element).attributes[eventNameToAttrName(event.type)];
if (expression != null) {
try {
var scope = getScope(element);
if (scope != null) scope.eval(expression);
} catch (e, s) {
exceptionHandler(e, s);
}
}
element = element.parentNode;
}
}

Scope getScope(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;
}
element = element.parentNode;
}
return null;
}

/**
* Converts event name into attribute. Event named 'someCustomEvent' needs to
* be transformed into on-some-custom-event.
*/
static String eventNameToAttrName(String eventName) {
var part = eventName.replaceAllMapped(new RegExp("([A-Z])"), (Match match) {
return '-${match.group(0).toLowerCase()}';
});
return 'on-${part}';
}

/**
* Converts attribute into event name. Attribute 'on-some-custom-event'
* corresponds to event named 'someCustomEvent'.
*/
static String attrNameToEventName(String attrName) {
var part = attrName.replaceAll("on-", "");
part = part.replaceAllMapped(new RegExp(r'\-(\w)'), (Match match) {
return match.group(0).toUpperCase();
});
return part.replaceAll("-", "");
}
}
2 changes: 2 additions & 0 deletions lib/core_dom/module.dart
Expand Up @@ -21,6 +21,7 @@ part 'compiler.dart';
part 'directive.dart';
part 'directive_map.dart';
part 'element_binder.dart';
part 'event_handler.dart';
part 'http.dart';
part 'ng_mustache.dart';
part 'node_cursor.dart';
Expand Down Expand Up @@ -60,6 +61,7 @@ class NgCoreDomModule extends Module {
type(DirectiveSelectorFactory);
type(ElementBinderFactory);
type(NgElement);
type(EventHandler);
}
}

Expand Down
3 changes: 3 additions & 0 deletions lib/core_dom/selector.dart
Expand Up @@ -298,6 +298,9 @@ class DirectiveSelector {

// Select [attributes]
element.attributes.forEach((attrName, value) {
if (attrName.startsWith("on-")) {
binder.onEvents[attrName] = value;
}
attrs[attrName] = value;
for (var k = 0; k < attrSelector.length; k++) {
_ContainsSelector selectorRegExp = attrSelector[k];
Expand Down
4 changes: 2 additions & 2 deletions lib/core_dom/tagging_compiler.dart
Expand Up @@ -51,7 +51,7 @@ class TaggingCompiler implements Compiler {

var taggedElementBinder = null;
int taggedElementBinderIndex = parentElementBinderOffset;
if (elementBinder.hasDirectives || elementBinder.hasTemplate) {
if (elementBinder.hasDirectivesOrEvents || elementBinder.hasTemplate) {
taggedElementBinder = _addBinder(elementBinders,
new TaggedElementBinder(elementBinder, parentElementBinderOffset));
taggedElementBinderIndex = elementBinders.length - 1;
Expand All @@ -78,7 +78,7 @@ class TaggingCompiler implements Compiler {
elementBinder;

if (elementBinder != null &&
elementBinder.hasDirectives &&
elementBinder.hasDirectivesOrEvents &&
(node.parentNode != null && templateCursor.current.parentNode != null)) {
if (directParentElementBinder == null) {

Expand Down
2 changes: 1 addition & 1 deletion lib/core_dom/tagging_view_factory.dart
Expand Up @@ -18,7 +18,7 @@ class TaggingViewFactory implements ViewFactory {
var timerId;
try {
assert((timerId = _perf.startTimer('ng.view')) != false);
var view = new View(nodes);
var view = new View(nodes, injector.get(EventHandler));
_link(view, nodes, elementBinders, injector);
return view;
} finally {
Expand Down
10 changes: 8 additions & 2 deletions lib/core_dom/view.dart
Expand Up @@ -13,7 +13,13 @@ part of angular.core.dom;
*/
class View {
final List<dom.Node> nodes;
View(this.nodes);
final EventHandler eventHandler;

View(this.nodes, this.eventHandler);

void registerEvent(String eventName) {
eventHandler.register(eventName);
}
}

/**
Expand Down Expand Up @@ -58,4 +64,4 @@ class ViewPort {
insertAfter == null
? placeholder
: insertAfter.nodes[insertAfter.nodes.length - 1];
}
}
5 changes: 4 additions & 1 deletion lib/core_dom/view_factory.dart
Expand Up @@ -49,7 +49,7 @@ class WalkingViewFactory implements ViewFactory {
var timerId;
try {
assert((timerId = _perf.startTimer('ng.view')) != false);
var view = new View(nodes);
var view = new View(nodes, injector.get(EventHandler));
_link(view, nodes, elementBinders, injector);
return view;
} finally {
Expand Down Expand Up @@ -231,9 +231,12 @@ class _ComponentFactory implements Function {
var shadowModule = new Module()
..type(type)
..type(NgElement)
..type(EventHandler)
..value(Scope, shadowScope)
..value(TemplateLoader, templateLoader)
..value(dom.ShadowRoot, shadowDom)
..value(dom.Element, null)
..value(dom.Node, shadowDom)
..factory(ElementProbe, (_) => probe);
shadowInjector = injector.createChild([shadowModule], name: _SHADOW);
probe = _expando[shadowDom] = new ElementProbe(
Expand Down
4 changes: 3 additions & 1 deletion lib/core_dom/walking_compiler.dart
Expand Up @@ -38,7 +38,9 @@ class WalkingCompiler implements Compiler {
}
}

if (elementBinder.hasDirectives) binder = elementBinder;
if (elementBinder.hasDirectivesOrEvents) {
binder = elementBinder;
}

if (elementBinders == null) elementBinders = [];
elementBinders.add(new ElementBinderTreeRef(templateCursor.index,
Expand Down
2 changes: 2 additions & 0 deletions lib/mock/module.dart
Expand Up @@ -41,6 +41,8 @@ class AngularMockModule extends Module {
type(Probe);
type(Logger);
type(MockHttpBackend);
value(Element, document.body);
value(Node, document.body);
factory(HttpBackend, (Injector i) => i.get(MockHttpBackend));
factory(NgZone, (_) {
return new NgZone()
Expand Down
15 changes: 13 additions & 2 deletions lib/mock/test_bed.dart
Expand Up @@ -11,13 +11,13 @@ class TestBed {
final Scope rootScope;
final Compiler compiler;
final Parser parser;

final Expando expando;

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

TestBed(this.injector, this.rootScope, this.compiler, this.parser);
TestBed(this.injector, this.rootScope, this.compiler, this.parser, this.expando);


/**
Expand Down Expand Up @@ -88,4 +88,15 @@ class TestBed {
triggerEvent(element, 'change');
rootScope.apply();
}

getProbe(Node node) {
while (node != null) {
ElementProbe probe = expando[node];
if (probe != null) return probe;
node = node.parent;
}
throw 'Probe not found.';
}

getScope(Node node) => getProbe(node).scope;
}
108 changes: 108 additions & 0 deletions test/core_dom/event_handler_spec.dart
@@ -0,0 +1,108 @@
library event_handler_spec;

import '../_specs.dart';

@NgController(selector: '[foo]', publishAs: 'ctrl')
class FooController {
var description = "desc";
var invoked = false;
}

@NgComponent(selector: 'bar',
template: '''
<div>
<span on-abc="ctrl.invoked=true;"></span>
<content></content>
</div>
''',
publishAs: 'ctrl')
class BarComponent {
var invoked = false;
BarComponent(RootScope scope) {
scope.context['ctrl'] = this;
}
}

main() {
describe('EventHandler', () {
beforeEachModule((Module module) {
module..type(FooController);
module..type(BarComponent);
module..value(Element, document.body);
});

// Not sure why this doesn't work.
afterEach(() {
document.body.children.clear();
});

it('shoud register and handle event', inject((TestBed _) {
document.body.append(_.compile(
'''<div foo>
<div on-abc="ctrl.invoked=true;"></div>
</div>'''));

document.querySelector('[on-abc]').dispatchEvent(new Event('abc'));
var fooScope = _.getScope(document.querySelector('[foo]'));
expect(fooScope.context['ctrl'].invoked).toEqual(true);
document.body.children.clear();
}));

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

document.querySelector('[on-my-new-event]').dispatchEvent(new Event('myNewEvent'));
var fooScope = _.getScope(document.querySelector('[foo]'));
expect(fooScope.context['ctrl'].invoked).toEqual(true);
document.body.children.clear();
}));

it('shoud have model updates applied correctly', inject((TestBed _) {
document.body.append(_.compile(
'''<div foo>
<div on-abc='ctrl.description="new description";'>{{ctrl.description}}</div>
</div>'''));
var el = document.querySelector('[on-abc]');
el.dispatchEvent(new Event('abc'));
_.rootScope.apply();
expect(el.text).toEqual("new description");
document.body.children.clear();
}));

it('shoud register event when shadow dom is used', async((TestBed _) {
document.body.append(_.compile('<bar></bar>'));

microLeap();

var shadowRoot = _.rootElement.shadowRoot;
var span = shadowRoot.querySelector('span');
span.dispatchEvent(new CustomEvent('abc'));
var ctrl = _.rootScope.context['ctrl'];
expect(ctrl.invoked).toEqual(true);
document.body.children.clear();
}));

it('shoud handle event within content only once', async(inject((TestBed _) {
document.body.append(_.compile(
'''<div foo>
<bar>
<div on-abc="ctrl.invoked=true;"></div>
</bar>
</div>'''));

microLeap();

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

var fooScope = _.getScope(document.querySelector('[foo]'));
expect(fooScope.context['ctrl'].invoked).toEqual(true);
document.body.children.clear();
})));
});
}

0 comments on commit c28e6a0

Please sign in to comment.