How does jQuery.on() function?
Necromancing:
Just in case anybody needs to replace JQuery on/live with Vanilla-JavaScript:
TypeScript:
/// attach an event handler, now or in the future,
/// for all elements which match childselector,
/// within the child tree of the element maching parentSelector.
export function subscribeEvent(parentSelector: string | Element
, eventName: string
, childSelector: string
, eventCallback)
{
if (parentSelector == null)
throw new ReferenceError("Parameter parentSelector is NULL");
if (childSelector == null)
throw new ReferenceError("Parameter childSelector is NULL");
// nodeToObserve: the node that will be observed for mutations
let nodeToObserve: Element = <Element>parentSelector;
if (typeof (parentSelector) === 'string')
nodeToObserve = document.querySelector(<string>parentSelector);
let eligibleChildren: NodeListOf<Element> = nodeToObserve.querySelectorAll(childSelector);
for (let i = 0; i < eligibleChildren.length; ++i)
{
eligibleChildren[i].addEventListener(eventName, eventCallback, false);
} // Next i
// https://stackoverflow.com/questions/2712136/how-do-i-make-this-loop-all-children-recursively
function allDescendants(node: Node)
{
if (node == null)
return;
for (let i = 0; i < node.childNodes.length; i++)
{
let child = node.childNodes[i];
allDescendants(child);
} // Next i
// IE 11 Polyfill
if (!Element.prototype.matches) Element.prototype.matches = Element.prototype.msMatchesSelector;
if ((<Element>node).matches)
{
if ((<Element>node).matches(childSelector))
{
// console.log("match");
node.addEventListener(eventName, eventCallback, false);
} // End if ((<Element>node).matches(childSelector))
// else console.log("no match");
} // End if ((<Element>node).matches)
// else console.log("no matchfunction");
} // End Function allDescendants
// Callback function to execute when mutations are observed
let callback:MutationCallback = function (mutationsList: MutationRecord[], observer: MutationObserver)
{
for (let mutation of mutationsList)
{
// console.log("mutation.type", mutation.type);
// console.log("mutation", mutation);
if (mutation.type == 'childList')
{
for (let i = 0; i < mutation.addedNodes.length; ++i)
{
let thisNode: Node = mutation.addedNodes[i];
allDescendants(thisNode);
} // Next i
} // End if (mutation.type == 'childList')
// else if (mutation.type == 'attributes') { console.log('The ' + mutation.attributeName + ' attribute was modified.');
} // Next mutation
}; // End Function callback
// Options for the observer (which mutations to observe)
let config = { attributes: false, childList: true, subtree: true };
// Create an observer instance linked to the callback function
let observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(nodeToObserve, config);
} // End Function subscribeEvent
JavaScript:
/// attach an event handler, now or in the future,
/// for all elements which match childselector,
/// within the child tree of the element maching parentSelector.
function subscribeEvent(parentSelector, eventName, childSelector, eventCallback) {
if (parentSelector == null)
throw new ReferenceError("Parameter parentSelector is NULL");
if (childSelector == null)
throw new ReferenceError("Parameter childSelector is NULL");
// nodeToObserve: the node that will be observed for mutations
var nodeToObserve = parentSelector;
if (typeof (parentSelector) === 'string')
nodeToObserve = document.querySelector(parentSelector);
var eligibleChildren = nodeToObserve.querySelectorAll(childSelector);
for (var i = 0; i < eligibleChildren.length; ++i) {
eligibleChildren[i].addEventListener(eventName, eventCallback, false);
} // Next i
// https://stackoverflow.com/questions/2712136/how-do-i-make-this-loop-all-children-recursively
function allDescendants(node) {
if (node == null)
return;
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
allDescendants(child);
} // Next i
// IE 11 Polyfill
if (!Element.prototype.matches)
Element.prototype.matches = Element.prototype.msMatchesSelector;
if (node.matches) {
if (node.matches(childSelector)) {
// console.log("match");
node.addEventListener(eventName, eventCallback, false);
} // End if ((<Element>node).matches(childSelector))
// else console.log("no match");
} // End if ((<Element>node).matches)
// else console.log("no matchfunction");
} // End Function allDescendants
// Callback function to execute when mutations are observed
var callback = function (mutationsList, observer) {
for (var _i = 0, mutationsList_1 = mutationsList; _i < mutationsList_1.length; _i++) {
var mutation = mutationsList_1[_i];
// console.log("mutation.type", mutation.type);
// console.log("mutation", mutation);
if (mutation.type == 'childList') {
for (var i = 0; i < mutation.addedNodes.length; ++i) {
var thisNode = mutation.addedNodes[i];
allDescendants(thisNode);
} // Next i
} // End if (mutation.type == 'childList')
// else if (mutation.type == 'attributes') { console.log('The ' + mutation.attributeName + ' attribute was modified.');
} // Next mutation
}; // End Function callback
// Options for the observer (which mutations to observe)
var config = { attributes: false, childList: true, subtree: true };
// Create an observer instance linked to the callback function
var observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(nodeToObserve, config);
} // End Function subscribeEvent
I assume your question is about the Event Delegation version of .on
.
In JavaScript, most events bubble up in the DOM hierarchy. That means, when an event that can bubble fires for an element, it will bubble up to the DOM until document
level.
Consider this basic markup:
<div>
<span>1</span>
<span>2</span>
</div>
Now we apply event delegation:
$('div').on('click', 'span', fn);
The event handler is attached solely to the div
element. As the span
are inside of the div
, any click in the spans will bubble up to the div
, firing the div
's click handler. At that moment, all that is left is checking whether the event.target
(or any of the elements between the target and the delegateTarget
) matches the delegation target selector.
Let's make the a bit more complex:
<div id="parent">
<span>1</span>
<span>2 <b>another nested element inside span</b></span>
<p>paragraph won't fire delegated handler</p>
</div>
The basic logic is as follows:
//attach handler to an ancestor
document.getElementById('parent').addEventListener('click', function(e) {
//filter out the event target
if (e.target.tagName.toLowerCase() === 'span') {
console.log('span inside parent clicked');
}
});
Though the above will not match when event.target
is nested inside your filter. We need some iteration logic:
document.getElementById('parent').addEventListener('click', function(e) {
var failsFilter = true,
el = e.target;
while (el !== this && (failsFilter = el.tagName.toLowerCase() !== 'span') && (el = el.parentNode));
if (!failsFilter) {
console.log('span inside parent clicked');
}
});
Fiddle
edit: Updated code to match only descendant elements as jQuery's .on
does.
Note: These snippets are for explanatory purposes, not be used in real world. Unless you don't plan to support old IE, of course. For old IE you would need to feature test against addEventListener
/attachEvent
as well as event.target || event.srcElement
, and possibly some other quirks such as checking whether the event object is passed to handler function or available in the global scope. Thankfully jQuery does all that seamlessly behind the scenes for us. =]