Faking the Future

written by jonathantneal on June 20, 2012 in JavaScript with 2 comments

Ah, querySelector, the native answer to the best part of jQuery, or rather Sizzle.querySelector returns the first matching element descended from the element on which it is used. When used as querySelectorAll, it returns the entire list of matching descendant elements. It is elegant, and yet so useful. It’s proof that JavaScript just keeps getting better and better.

document.getElementsByTagName('h1')[0]; // the old way

document.querySelector('h1'); // the new way

someElement.getElementsByClassName('foo'); // the old way, which never worked until IE9

someElement.querySelectorAll('.foo'); // the new way, which even works in IE8

What bums me out about querySelector is that it implies some other APIs that are missing. For example, if we wanted to check whether an element is matched by a selector, we would use matchesSelector. That kind of functionality would be especially useful in event delegation where the returning target is unknown. Unfortunately, matchesSelector is unavailable in IE8, Safari 4, and, as of mid-2012 exclusively vendor prefixed while the Selectors API 2 remains a draft.

document.addEventListener('click', function (e) {
	var matches = e.target.matchesSelector('a[rel="external"]'); // doesn't work anywhere in 2012.

	if (matches) {
		// cancel the default action
		e.preventDefault();

		// do something special, like open the anchor in a new window
		open(e.target, '_blank');
	}

	// (moz|ms|o|webkit)MatchesSelector all work though.
});

One way around this almost-standard API is to polyfill the eventual implementation with either the vendor version or something original. This is faking the future.

this.Element && (function (ElementPrototype) {
	ElementPrototype.matchesSelector = ElementPrototype.matchesSelector ||
	ElementPrototype.mozMatchesSelector ||
	ElementPrototype.msMatchesSelector ||
	ElementPrototype.oMatchesSelector ||
	ElementPrototype.webkitMatchesSelector ||
	function matchesSelector(selector) {
		var
		results = this.parentNode.querySelectorAll(selector),
		resultsIndex = -1;

		while (results[++resultsIndex] && results[resultsIndex] != this) {}

		return !!results[resultsIndex];
	};
})(Element.prototype);

With matchesSelector polyfilled, we can check whether an element matches a selector. Using querySelector we can return a descendant matching a selector. Now, what if we wanted to get an ancestor that matched a selector, similar to jQuery’s closest() method? we would need an ancestorQuerySelector method. ancestorQuerySelector would return the first matching ancestor of the element on which it is used. When used as ancestorQuerySelectorAll, it would return the entire list of matching ancestor elements.

document.addEventListener('click', function (e) {
	var
	// set the selector
	selector = 'a[rel="external"]',

	// get the closest matching element
	closestElement = e.target.matchesSelector(selector) ? e.target : e.target.ancestorQuerySelector(selector);

	if (closestElement) {
		// cancel the default action
		e.preventDefault();

		// do something special, like open the anchor in a new window
		open(e.target, '_blank');
	}
});

Unfortunately, ancestorQuerySelector doesn’t have a native implementation in any browser. So, if we were to add it? And what if we were to check for native or vendor-prefixed functionality first? Is that still polyfilling? Can a developer make such a suggestion without writing a selector draft? Are we faking the future or fauxing the future?

this.Element && (function (ElementPrototype, polyfill) {
	function NodeList() { [polyfill] }
	NodeList.prototype.length = ArrayPrototype.length;

	ElementPrototype.ancestorQuerySelectorAll = ElementPrototype.ancestorQuerySelectorAll ||
	ElementPrototype.mozAncestorQuerySelectorAll ||
	ElementPrototype.msAncestorQuerySelectorAll ||
	ElementPrototype.oAncestorQuerySelectorAll ||
	ElementPrototype.webkitAncestorQuerySelectorAll ||
	function ancestorQuerySelectorAll(selector) {
		for (var cite = this, newNodeList = new NodeList; cite = cite.parentElement;) {
			if (cite.matchesSelector(selector)) ArrayPrototype.push.call(newNodeList, cite);
		}

		return newNodeList;
	};

	ElementPrototype.ancestorQuerySelector = ElementPrototype.ancestorQuerySelector ||
	ElementPrototype.mozAncestorQuerySelector ||
	ElementPrototype.msAncestorQuerySelector ||
	ElementPrototype.oAncestorQuerySelector ||
	ElementPrototype.webkitAncestorQuerySelector ||
	function ancestorQuerySelector(selector) {
		return this.ancestorQuerySelectorAll(selector)[0] || null;
	};
})(Element.prototype);