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.

(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…


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:


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:


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:


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:


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:


live('.mainnav a', 'click', someCallback);

And your HTML looks like this:


<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:


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:


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:


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 :smiley:

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:

<!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>

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:

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):


function live(selector, eventType, callback) {
    document.addEventListener(eventType, function (e) {
        if (e.target.webkitMatchesSelector(selector)) {
            callback.call(e.target, e);
        }
    }, false);
}

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:


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.

Here’s kind of how your HTML looked in your first example:

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):


          | - 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:

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:


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?

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!

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:


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:


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/showthread.php?786594-Vanilla-Js-version-of-.live()&p=4965599&viewfull=1#post4965599. But, with updating the code to:


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:

function live(selector, eventType, callback) {
    document.addEventListener(eventType, function (e) {
        if (e.target.webkitMatchesSelector(selector)) {
            callback.call(e.target, e);
        }
    }, false);
}