SitePoint Sponsor

User Tag List

Results 1 to 8 of 8

Hybrid View

  1. #1
    SitePoint Addict
    Join Date
    Dec 2005
    Posts
    336
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)

    Vanilla Js version of $.live()?

    Pretty simple question and google/stackoverflow isn't helpful. I don't want to try and duplicate efforts if it's already been done.

    jQuery/Zepto source relies on internal functions, so please don't recommend that.

  2. #2
    SitePoint Addict sdleihssirhc's Avatar
    Join Date
    Feb 2009
    Posts
    387
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    (Sorry if this sounds condescending at all, I just wrote my own "event delegation" function, and the experience sucked, so now I share my wisdom with the world.)

    Assuming you want a function that works like this...

    Code JavaScript:
    function live(selector, eventType, callback) {
        // ...
    }

    ...there are three steps to this problem:

    1. Create a function that normalizes the IE/W3C event models
    2. Create a function that will tell you whether or not an element matches a selector
    3. What if the event actually takes place in a child of the element we're trying to match?


    Step One: Normalizing Event Models

    When you add a listener using your new live function, is it possible you might ever want to remove it? If not, things get much, much simpler.

    And, depending on what you want to do with your live function, almost every other way that the IE/W3C models differ could very well be irrelevant (the slightly different event objects that are generated; what "this" refers to; the order in which the callbacks are executed; memory leaks in IE6/7; how to handle the same callback being added to the same element for the same event type; mouseenter/focusin support; etc)

    My point is, the first step is a whole issue in and of itself. That I will skirt. I'm going to assume that you've got at least a cross-browser "addEvent" function that looks like this:

    Code JavaScript:
    function addEvent(elem, type, callback) {
        // ...
    }

    How you get there is up to you.

    Step Two: Element/Selector Matching

    Good browsers like Chrome and Firefox have these cool methods called "webkitMatchesSelector" and "mozMatchesSelector" (Opera and [I think] IE10 have them as well, with the "o" and "ms" prefixes). The real function that we might get someday is supposed to be "matchesSelector," but I think you have to use the prefixes for now. They work like this:

    Code JavaScript:
    var yourElem = document.getElementById('yourElem');
    yourElem.webkitMatchesSelector('#yourElem'); // returns "true" in Chrome and Safari

    But what about browsers that don't support it at all? A cheap way to fake it is to use "querySelectorAll" with your selector, and then see if your element is in the returned StaticNodeList:

    Code JavaScript:
    var possibles = document.querySelectorAll(yourSelector),
        i = possibles.length;
    while (i) {
        i -= 1;
        if (yourElem === possibles[i]) {
            return true;
        }
    }

    But then what about browsers that don't support "querySelectorAll" either? When I was doing this myself, I decided to just not support them. That essentially means ignoring IE6/7. If that's not an option, again, you'll have to figure out how to implement your own custom "element matches selector" function.

    So for our purposes, I'm assuming you got yourself a nice "matches" function that looks like this:

    Code JavaScript:
    function matches(elem, selector) {
        // returns "true" or "false"
    }

    Step Three: Wherein I Curse Event Bubbling

    Let's say you want to use your live function like so:

    Code JavaScript:
    live('.mainnav a', 'click', someCallback);

    And your HTML looks like this:

    HTML Code:
    <li class="mainnav">
        <a href="#">This <span>is</span> fun!</a>
    </li>
    Our live function is going to be relying on "e.target"/"window.event.srcElement" for the element that we match against the selector. But when you click on the word "is," you're actually clicking on the span, not the a. So when we try to match the element and the selector, it will fail.

    The only way to solve this rickum-rackum problem is to get every ancestor of the target element, and run all of them against the selector:

    Code JavaScript:
    function getAncestors(elem) {
        var bucket = [];
        do {
            bucket[bucket.length] = elem;
            elem = elem.parentNode;
        } while (elem);
        return bucket;
    }

    Putting It All Together

    What this is all building up to is a function that will wrap a new method around our callback, and only execute the callback if the target element of an event matches a given selector. This is basically how I did it:

    Code JavaScript:
    function liveWrapper(selector, method) {
        return function (e) {
            e = e || window.event;
            var target = e.target || e.srcElement,
                ancestors = getAncestors(target),
                i = 0, l = ancestors.length,
                elem;
     
            while (i < l) {
                elem = ancestors[i];
                if (matches(elem, selector)) {
                    method.call(elem, e);
                    break;
                }
                i += 1;
            }
        };
    }

    And once we have that, we can create our live function:

    Code JavaScript:
    function live(selector, eventType, callback) {
        var callback2 = liveWrapper(callback);
        addEvent(document, eventType, callback2);
    }

    As I said above, this assumes that once the event is attached, you won't need to remove it; removing complicates things considerably.

    This is the part where you tell me that I completely misunderstood the question, and this essay doesn't help at all
    I'm the web overlord for Graphic Business Systems

  3. #3
    SitePoint Addict
    Join Date
    Dec 2005
    Posts
    336
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    Thanks for this. I am digesting it now. I don't need IE/backwards compatibility (will be for iOS/Android) and I don't need to remove the events. I am looking at this to try and simply it further if I can - ie can I do without the matches function and do webkitMatchesSelector?

    So far based on your post, I did this basic test:
    HTML Code:
    <!DOCTYPE html>
    <html>
    	<head>
    		<title></title>
    		<script>
    			function addEvent(elm, evt, fn) {
    				elm.addEventListener(evt, fn, false);
    			}
    			function matches(elm, selectors) {
    				var possibles = document.querySelectorAll(selectors),
    					i = possibles.length;
    				while (i) {
    					i -= 1;
    					if (elm === possibles[i]) {
    						return true;
    					}
    				}
    			}
    			function getAncestors(elm) {
    				var bucket = [];
    				do {
    					bucket[bucket.length] = elm;
    					elm = elm.parentNode;
    				} while (elm);
    				return bucket;
    			}
    			function liveWrapper(selector, method) {
    				return function (e) {
    					var target = e.target,
    						ancestors = getAncestors(target),
    						i = 0,
    						l = ancestors.length,
    						elem;
    
    					while (i < l) {
    						elem = ancestors[i];
    						if (matches(elem, selector)) {
    							method.call(elem, e);
    							break;
    						}
    						i += 1;
    					}
    				};
    			}
    			function live(items, evt, fn) {
    				var callback2 = liveWrapper(items, fn);
    				addEvent(document, evt, callback2);
    
    			}
    		</script>		
    	</head>
    	<body>
    		<p><a href="" id="clickMe">Click me for some information</a></p>
    		<div id="test"></div>
    		<script>
    			var clickMe = document.getElementById('clickMe');
    				clickMe.addEventListener(
    					'click', 
    					function(e) {
    						e.preventDefault();
    						var testDiv = document.getElementById('test');
    							testDiv.innerHTML = '<ul><li><a href="" class="link">Link 1</a></li><li><a href="" class="link">Link 2</a></li><li><a href="" class="link">Link 3</a></li><li><a href="" class="link">Link 4</a></li></ul>';
    					}, 
    					false
    				);
    				
    				
    			live(
    				'.link', 
    				'click', 
    				function(e) {
    					e.preventDefault();
    					alert('You clicked on ' + this.innerHTML);
    				}
    			);
    			
    		</script>
    	</body>
    </html>

  4. #4
    SitePoint Addict sdleihssirhc's Avatar
    Join Date
    Feb 2009
    Posts
    387
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    Things are looking good. I made a jsFiddle from your code and tested it in Chrome--it worked! But heck yes, things can be simplified even further:

    • Normalize the event model: Not necessary. You're only targeting iOS and Android, which both use addEventListener; so that "addEvent" function can just be removed.
    • Element matching selector: Probably not necessary. According to caniuse.com, the "webkitMatchesSelector" function is only supported in iOS 4+ and Android 2.2+. The good news is if you do need to support earlier versions, they all have "querySelectorAll" (again, according to caniuse.com).
    • Event bubbling: In your example, you're only targeting elements that have a class of ".link"; and none of those elements had any children. So no matter where the user clicks on the link, that <a> element will show up as the event's target. If this is always going to be the case, then you don't need the getAncestors function. That's only necessary if the element you're trying to target (in this case, <a> elements with the class ".link") will have any elements inside them (like a <span> or <img>).


    So, assuming that you don't care about early versions of iOS/Android and that your links will never contain child elements, you can simplify the code all the way down to the following (and here's another fiddle proving it still works):

    Code JavaScript:
    function live(selector, eventType, callback) {
        document.addEventListener(eventType, function (e) {
            if (e.target.webkitMatchesSelector(selector)) {
                callback.call(e.target, e);
            }
        }, false);
    }
    I'm the web overlord for Graphic Business Systems

  5. #5
    SitePoint Addict
    Join Date
    Dec 2005
    Posts
    336
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    Thanks, that was what I was looking for. The only issue is I don't know if my code will have children to them, so I will need to account for that as well.

    Then again, what is your example/test with children? Pretty quickly I added a nested ul/li with a p/a in the li element:
    Code:
    testDiv.innerHTML = '<ul><li><a href="" class="link">Link 1</a></li><li><a href="" class="link">Link 2</a><ul><li><p>Some text and some more. <a href="" class="innerLink">Inner Link</a></p></li></ul></li><li><a href="" class="link">Link 3</a></li><li><a href="" class="link">Link 4</a></li></ul>';
    For the new HTML: The webkit code you provided above works with this. The initial code you gave me doesn't.

  6. #6
    SitePoint Addict sdleihssirhc's Avatar
    Join Date
    Feb 2009
    Posts
    387
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    Here's kind of how your HTML looked in your first example:

    Code:
    ul - li - a.link
    Very simple. You have an unordered list that contains list items, and each list item contains an anchor with a class of "link." Note that, because the <a> doesn't contain any other elements, we don't need to worry about checking ancestors.

    Here's your new, nested structure (at least, just for the <li> that has a <ul> inside it):

    Code:
              | - a.link
    ul - li - |
              | - ul - li - p - a.innerLink
    Assuming that we're still just trying to target elements with a class of "link," everything should still work as expected. You click on the anchor, and it alerts (here's the fiddle). You inserted the new <ul> into the <li>, which we don't care about targeting in this case, so nothing should really change.

    The problem that the ancestor thing is trying to solve is when you have a structure like so:

    Code:
    ul - li - a.link - span
    You want to target the <a>, but the click actually takes place on the <span>. That's when you would need the getAncestors function. And if that might happen, then the following change should work for you:

    Code JavaScript:
    function getAncestors(elm) {
        var bucket = [];
        do {
            bucket[bucket.length] = elm;
            elm = elm.parentNode;
        } while (elm);
        return bucket;
    }
     
    function live(selector, eventType, callback) {
        document.addEventListener(eventType, function (e) {
            var ancestors = getAncestors(e.target),
                i = 0, l = ancestors.length;
     
            while (i < l) {
                if (ancestors[i].webkitMatchesSelector(selector)) {
                    method.call(ancestors[i], e);
                }
                i += 1;
            }
        }, false);
    }

    Can you post the code that you were having trouble with?
    I'm the web overlord for Graphic Business Systems

  7. #7
    SitePoint Addict
    Join Date
    Dec 2005
    Posts
    336
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    Thanks again! Sorry I am running in and out. Can you give a code example of the a.link/span you were referring to - I think I need to see the code to make it 'click'? What didn't work was what you posted in the latest fiddle: http://jsfiddle.net/nsjzg/2/

    Again, thank you for this!

  8. #8
    SitePoint Addict
    Join Date
    Dec 2005
    Posts
    336
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    Ok I am doing some more testing on the a/span you noted here:
    You want to target the <a>, but the click actually takes place on the <span>
    Using the following, that adds a span after the a.innerLink, which I hope is what you referred to:
    HTML Code:
    testDiv.innerHTML = '<ul><li><a href="" class="link">Link 1</a></li><li><a href="" class="link">Link 2</a><ul><li><p>Some text and some more. <a href="" id="innerLink">Inner Link</a><span id="testSpan">More and more</span></p></li></ul></li><li><a href="" class="link">Link 3</a></li><li><a href="" class="link">Link 4</a></li></ul>';
    Using this code:
    HTML Code:
    live(
    	'.link', 
    	'click', 
    	function(e) {
    		e.preventDefault();
    		alert('You clicked on ' + this.innerHTML);
    	}
    );
    
    // Get the span's innerHTML after clicking .innerHTML
    live(
    	'.innerLink',
    	'click',
    	function(e) {
    		e.preventDefault();
    		var spanLink = document.getElementById('testSpan');
    		alert('You clicked on ' + spanLink.innerHTML);
    	}				
    );				
    
    // Get the .innerLink's HTML after clicking on the span
    live(
    	'#testSpan',
    	'click',
    	function(e) {
    		e.preventDefault();
    		var innerLinks = document.getElementById('.innerLink');
    		alert('You clicked on ' + innerLinks[i].innerHTML);
    	}		
    );
    All the codes work with the exception of the latest code you provided here: http://www.sitepoint.com/forums/show...=1#post4965599. But, with updating the code to:
    HTML Code:
    function getAncestors(elm) {
    	var bucket = [];
    	do {
    		bucket[bucket.length] = elm;
    		elm = elm.parentNode;
    	} while (elm);
    	return bucket;
    }
    
    function live(selector, eventType, callback) {
    	document.addEventListener(eventType, function (e) {
    		var ancestors = getAncestors(e.target);
    
    		for( i=0; i<ancestors.length; i++) {
    			if (ancestors[i].webkitMatchesSelector(selector)) {
    				callback.call(ancestors[i], e);
    			}
    		}
    	}, false);
    }
    All codes work... so.......

    So I *think* all is good with this code, unelss you tell me otherwise:
    HTML Code:
    function live(selector, eventType, callback) {
        document.addEventListener(eventType, function (e) {
            if (e.target.webkitMatchesSelector(selector)) {
                callback.call(e.target, e);
            }
        }, false);
    }


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •