Catching clicks with clickCatcher before your JavaScript files have loaded/events been applied

One of the key things to improve performance of a web site, real as well as perceived, is including your JavaScript files at the end of the document (as I described in Improve Your Web Site Performance – Tips & Tricks To Get A Good YSlow Rating). However, there is a drawback to that.

The problem

What happens is that if the user clicks anything in the document that you plan to assign an event handler to before your JavaScript files have loaded and have run, the web browser will run the default action for that element. This could, for instance, be a link element, resulting in the user being taken to another page, where you instead had meant to prevent that and get some information loaded dynamically via AJAX or something.

The solution, in theory

Recently, I have been discussing and working on this with my colleague David Billskog, and the proposed solution is to have a JavaScript block included in the head of the document (inline or included) where you make sure to catch all click events in the page. Then, you would want to filter those to make sure they were only on an element that you know you actually are going to apply your own event handler to.

And, finally, when the page has loaded/your event handlers have been assigned, you want to remove the previously assigned click catcher, iterate over all the elements that got clicked before that, and now trigger a true click event on them to trigger both their natural and applied events’ behavior.

The solution, in practice

Therefore, David and I started our work separately, me on a solution not needing any JavaScript library and he on a version based on jQuery (but could easily be adapted to any other JavaScript library). I’ll present my solution, the clickCatcher, below.

The clickCatcher is using a few handy script functions as its base:

  • addEvent to assign a click event to the document without overriding any other potentially assigned click handlers.
  • removeEvent to remove the above assigned event, so it doesn’t continue to catch click once the page has loaded.
  • fireEvent to fire a click event on all elements that were clicked before the JavaScript files had loaded.

The clickCatcher

(function () {
	clickCatcher = function () {
		var clicks = [],
			addClicks = function (evt) {
				var classCheck = /catch/,
					body = /body/i,
					target = (evt.target)? evt.target : evt.srcElement;

				while (!classCheck.test(target.className) && !body.test(target.nodeName)) {
					target = target.parentNode;
				}

				if (classCheck.test(target.className)) {
					clicks.push(target);
					if (evt.preventDefault) {
						evt.preventDefault();
					}
					evt.returnValue = false;
					return false;
				}
			},
			callClicks = function () {
				removeEvent(document, "click", addClicks);
				for (var i=0, il=clicks.length; i<il; i++) {
					fireEvent(clicks[i], "click");
				};
			},
			init = function () {
				addEvent(document, "click", addClicks);
				// Could be called here, but now called manually in script loaded later - adapt to your situation
				//addEvent(window, "load", callClicks);
			};

		return {
			init : init,
			callClicks : callClicks
		};	
	}();
	clickCatcher.init();
	return clickCatcher;
})();

The clickCatcher is wrapped in an anonymous self-invoking function, not to mess with the global scope of the web page and to create its own scope. This code can optionally also expose the clickCatcher object itself, if you want it accessible from elsewhere. It appends a click event handler to the document, and stores each click in an array if the element being clicked (or any of its parent elements) has the class catch (as in Link).

Once the page has loaded, you can have this called automatically, or you can trigger it manually by calling clickCatcher.callClicks();

The complete code, with the addEvent, removeEvent and fireEvent methods included, looks like this:

Code updated to “good ol'” event branching, because it’s all that’s needed for this scenario.

// By Robert Nyman, http://robertnyman.com/clickcatcher/ - This content is released under the MIT License: http://www.opensource.org/licenses/mit-license.php
(function () {
	function addEvent(elm, evt, func) {
		if (elm.addEventListener) {
			elm.addEventListener(evt, func, false);
		}
		else {
			elm.attachEvent(("on" + evt), func);
		}
	}
	
	function removeEvent(elm, evt, func) {
		if (elm.removeEventListener) {
			elm.removeEventListener(evt, func, false);
		}
		else {
			elm.detachEvent(("on" + evt), func);
		}
	}

	// fireEvent by Jehiah Czebotar, http://jehiah.cz/archive/firing-javascript-events-properly
	function fireEvent(element, event) {
		var evt;
		if (document.createEvent) {
			// dispatch for firefox + others
			evt = document.createEvent("HTMLEvents");
			evt.initEvent(event, true, true ); // event type,bubbling,cancelable
			return !element.dispatchEvent(evt);
		} else {
		// dispatch for IE
			evt = document.createEventObject();
			return element.fireEvent('on'+event,evt);
		}
	}

	clickCatcher = function () {
		var clicks = [],
			addClicks = function (evt) {
				var classCheck = /catch/,
					body = /body/i,
					target = (evt.target)? evt.target : evt.srcElement;

				while (!classCheck.test(target.className) && !body.test(target.nodeName)) {
					target = target.parentNode;
				}

				if (classCheck.test(target.className)) {
					clicks.push(target);
					if (evt.preventDefault) {
						evt.preventDefault();
					}
					evt.returnValue = false;
					return false;
				}
			},
			callClicks = function () {
				removeEvent(document, "click", addClicks);
				for (var i=0, il=clicks.length; i<il; i++) {
					fireEvent(clicks[i], "click");
				};
			},
			init = function () {
				addEvent(document, "click", addClicks);
				// Could be called here, but now called manually in script loaded later - adapt to your situation
				//addEvent(window, "load", callClicks);
			};

		return {
			init : init,
			callClicks : callClicks
		};	
	}();
	clickCatcher.init();
	return clickCatcher;
})();

You can test this code in the clickCatcher page.

The setTimeout trick

However, no matter if the triggering of the execution of the clicks is from within the clickCatcher object or called manually, you want to make sure this is the last one being called, i.e. when all events have been applied. An interesting result from David’s research is that you can control the call stack by using setTimeout and setting the time to 0, thus putting it last in the stack compared to other load event handlers of the exact same.

What this means is that if you, for instance, use addEvent to call something when the window has loaded, you could control it like this (probably only safe if they are in the same script block/JavaScript file – any feedback here is appreciated!):

	// Calling click handler
	setTimeout(function () {
		addEvent(window, "load", callClicks);
	}, 0);
	
	// My own function - will be called before the above function
	addEvent(window, "load", myCoolFunc);

A jQuery option

I mentioned before that David has been working on a jQuery version, which means that jQuery has to be included before you call it, but you can all include all other JavaScript files at the end of the document. He presents his approach and findings in clickCatcher with jQuery.

Feedback?

From our testing this seems to be a good and solid way to catch clicks before your JavaScript files have loaded, and still having the performance advantage of having them included at the bottom of the document. If you have any findings, addendum or similar, please let us know!

Posted in Developing,JavaScript,Technology |

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>