11

I have not seen the source of this function, but I wonder, does it work like this:

  1. Selects element(s) by their selectors
  2. Delegates the provided event-handlers to them
  3. Runs a setInterval on that selector and constantly un-delegating, and then re-delegating that same event all over

Or there is a pure-JavaScript DOM explanation to this?

Brian Tompsett - 汤莱恩
  • 5,195
  • 62
  • 50
  • 120

2 Answers2

19

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. =]

Fabrício Matté
  • 65,581
  • 23
  • 120
  • 159
  • well it's all OK about jQuery and how to use jQuery.on(), I am interested is it possible to write a similar snippet only on JavaScript and DOM ? –  Feb 27 '13 at 12:43
  • @Zlatan Yes it is. Though without sizzle it you won't have as many fancy delegation selectors. I'll add a pure JS one example. – Fabrício Matté Feb 27 '13 at 12:44
  • OK. Well I think you can write an example with: `document.querySelector()` or `document.querySelectorAll()` :) –  Feb 27 '13 at 12:46
  • Yes though I'll make it simpler. Here's the basic one: http://jsfiddle.net/38bKp/ I'll write one to handle nested elements and update the answer – Fabrício Matté Feb 27 '13 at 12:51
  • yes yes yes! Now I get the whole picture. I totally forgotten about EventListeners and their purpose! now I understand how .on() can work on newly created elements under one instance of DOM! thanks!! –  Feb 27 '13 at 12:56
  • 1
    @Zlatan No problem, updated answer with a more sturdy snippet. I can make an wrapper with a filter function callback that works similar to `.on` if you'd like, but I guess is to understand of reinventing the wheel. `:P` – Fabrício Matté Feb 27 '13 at 13:04
  • thanks one more.. I was thinking about this matter for months now, but decided to post question today.. non the less, about reinventing, as Jobs once said: `"Today, we're gonna reinvet the (iph)on!" :P` –  Feb 27 '13 at 13:09
3

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 
Stefan Steiger
  • 68,404
  • 63
  • 337
  • 408