Skip to content

Commit

Permalink
Add an Event.Handler class, plus Event.on and Element#on method…
Browse files Browse the repository at this point in the history
…s, for simplified event delegation. (sam, Tobie Langel, Andrew Dupont)
  • Loading branch information
savetheclocktower committed Mar 25, 2010
1 parent 91e5582 commit f64a331
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 11 deletions.
96 changes: 94 additions & 2 deletions src/dom/event.js
Expand Up @@ -852,14 +852,98 @@

return Event.extend(event);
}

/**
* class Event.Handler
*
* Creates an observer on an element that listens for a particular event on
* that element's descendants, optionally filtering by a CSS selector.
*
* This class simplifies the common "event delegation" pattern, in which one
* avoids adding an observer to a number of individual elements and instead
* listens on a _common ancestor_ element.
*
**/
Event.Handler = Class.create({
/**
* new Event.Handler(element, eventName[, selector], callback)
* - element (Element): The element to listen on.
* - eventName (String): An event to listen for. Can be a standard browser
* event or a custom event.
* - selector (String): A CSS selector. If specified, will call `callback`
* _only_ when it can find an element that matches `selector` somewhere
* in the ancestor chain between the event's target element and the
* given `element`.
* - callback (Function): The event handler function. Should expect two
* arguments: the event object _and_ the element that received the
* event. (If `selector` was given, this element will be the one that
* satisfies the criteria described just above; if not, it will be the
* one specified in the `element` argument).
*
* Instantiates an `Event.Handler`. **Will not** begin observing until
* [[Event.Handler#start]] is called.
**/
initialize: function(element, eventName, selector, callback) {
this.element = $(element);
this.eventName = eventName;
this.selector = selector;
this.callback = callback;
this.handler = this.handleEvent.bind(this);
},

/**
* Event.Handler#start -> Event.Handler
*
* Starts listening for events. Returns itself.
**/
start: function() {
Event.observe(this.element, this.eventName, this.handler);
return this;
},

/**
* Event.Handler#stop -> Event.Handler
*
* Stops listening for events. Returns itself.
**/
stop: function() {
Event.stopObserving(this.element, this.eventName, this.handler);
return this;
},

handleEvent: function(event) {
var element = this.selector ? event.findElement(this.selector) :
this.element;
if (element) this.callback.call(element, event, element);
}
});

/**
* Event.on(element, eventName, selector, callback) -> Event.Handler
*
* Listens for events on an element's descendants, optionally filtering
* to match a given CSS selector.
*
* Creates an instance of [[Event.Handler]], calls [[Event.Handler#start]],
* then returns that instance. Keep a reference to this returned instance if
* you later want to unregister the observer.
**/
function on(element, eventName, selector, callback) {
element = $(element);
if (Object.isFunction(selector) && Object.isUndefined(callback)) {
callback = selector, selector = null;
}

return new Event.Handler(element, eventName, selector, callback).start();
}

Object.extend(Event, Event.Methods);

Object.extend(Event, {
fire: fire,
observe: observe,
stopObserving: stopObserving
stopObserving: stopObserving,
on: on
});

Element.addMethods({
Expand Down Expand Up @@ -918,7 +1002,13 @@
* Element.stopObserving(@element[, eventName[, handler]]) -> Element
* See [[Event.stopObserving]].
**/
stopObserving: stopObserving
stopObserving: stopObserving,

/**
* Element.on(@element, evnetName[, selector], callback) -> Element
* See [[Event.on]].
**/
on: on
});

/** section: DOM
Expand Down Expand Up @@ -982,6 +1072,8 @@
* [[Element.stopObserving]].
**/
stopObserving: stopObserving.methodize(),

on: on.methodize(),

/**
* document.loaded -> Boolean
Expand Down
91 changes: 82 additions & 9 deletions test/functional/event.html
Expand Up @@ -8,19 +8,32 @@

<style type="text/css" media="screen">
/* <![CDATA[ */
body { margin:1em 2em; padding:0; font-size:0.8em }
body {
margin:1em 2em; padding:0; font-size:0.8em;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
hr { width:31.2em; margin:1em 0; text-align:left }
p { width:30em; margin:0.5em 0; padding:0.3em 0.6em; color:#222; background:#eee; border:1px solid silver; }
.subtest { margin-top:-0.5em }
/* p { width:30em; margin:0.5em 0; padding:0.3em 0.6em; color:#222; background:#eee; border:1px solid silver; }
*/ .subtest { margin-top:-0.5em }
.passed { color:green; border-color:olive }
.failed { color:firebrick; border-color:firebrick }
.button { padding:0.2em 0.4em; background:#ccc; border:1px solid #aaa }
#log { position:absolute; left:35em; top:5em; width:20em; font-size:13px !important }
fieldset { position:absolute; left:35em; top:5em; width:400px; font-size:13px !important }
h2 { font:normal 1.1em Verdana,Arial,sans-serif; font-style:italic; color:gray; margin-top:-1.2em }
h2 *, h2 a:visited { color:#444 }
h2 a:hover { color:blue }
a:visited { color:blue }
a:hover { color:red }

.test {

}

.test span {
border: 2px solid #999;
background-color: #ddd;
padding: 2px;
}
/* ]]> */
</style>

Expand Down Expand Up @@ -53,7 +66,10 @@
<body>
<h1>Prototype functional tests for the Event module</h1>

<div id="log">log empty</div>
<fieldset>
<legend>Log</legend>
<div id="log"></div>
</fieldset>

<p id="basic">A basic event test - <strong>click here</strong></p>
<p id="basic_remove" class="subtest"><strong>click</strong> to stop observing the first test</p>
Expand All @@ -63,13 +79,13 @@ <h1>Prototype functional tests for the Event module</h1>
<script type="text/javascript">
var basic_callback = function(e){
$('basic').passed();
if ($('basic_remove')) $('basic_remove').show()
else $('basic').failed()
if ($('basic_remove')) $('basic_remove').show();
else $('basic').failed();
log(e);
}
$('basic').observe('click', basic_callback)
$('basic_remove').observe('click', function(e){
el = $('basic')
$('basic_remove').observe('click', function(e) {
var el = $('basic');
el.passed('This test should now be inactive (try clicking)')
el.stopObserving('click')
$('basic_remove').remove()
Expand Down Expand Up @@ -263,5 +279,62 @@ <h1>Prototype functional tests for the Event module</h1>
log(e);
})
</script>

<div id="delegation_container">
Event delegation
<ul>
<li>
<span class="delegation-child-1">Child 1 (click)</span>
</li>
<li>
<span class="delegation-child-2">Child 2 (mouseover)</span>
</li>
<li class="delegation-child-3">
<span>Child 3 (mouseup)</span>
</li>
</ul>

Results:

<ul id="delegation_results">
<li id="delegation_result_1">Test 1</li>
<li id="delegation_result_2">Test 2</li>
<li id="delegation_result_3">Test 3</li>
</ul>
</div>
<script type="text/javascript">
var msg = "Passed. Click to unregister.";
var clickMsg = "Now try original event again to ensure observation was stopped."
var observer1 = $('delegation_container').on('click', '.delegation-child-1', function() {
var result = $('delegation_results').down('li', 0);
result.passed(msg + " (" + ((new Date).toString())) + ")";
result.observe('click', function() {
this.update(clickMsg);
observer1.stop();
});
});

var observer2 = $(document).on('mouseover', '.delegation-child-2', function() {
var result = $('delegation_results').down('li', 1);
result.passed(msg + " (" + ((new Date).toString())) + ")";
result.observe('click', function() {
this.update(clickMsg);
observer2.stop();
});
})

var observer3 = $('delegation_container').on('mouseup', '.delegation-child-3', function() {
var result = $('delegation_results').down('li', 2);
result.passed(msg + " (" + ((new Date).toString())) + ")";
result.observe('click', function() {
this.update(clickMsg);
observer3.stop();
});
});


</script>


</body>
</html>

0 comments on commit f64a331

Please sign in to comment.