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, https://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!

11 Comments

  • Tino Zijdel says:

    I don't know what to think of this; it feels awkward having to add additional code to work around a problem caused by an attempt to improve performance. In my opinion this is a tell-tale sign that you took your efforts for improving performance too far and that you should reconsider adding those functions that are necessary for the page to function as intended in javascript-enabled browsers back into the head-section.

    Another problem I have with this solution is the fact that clicking something on the page and the click being queued leaves the user without any visible pointer that something should happen but is waiting for some script to be loaded. Ofcourse this could be remedied by f.i. placing a throbber over or nearby the clicked item, but that will mean even more code and complexity.

    Last I wouldn't recommend using Resigs' addEvent because it has several problems (John doesn't even recommend it himself there). Besides the problem with relying on function decompilation testing for MSIE's proprietary method first is i.m.o. the wrong order.

  • Robert Nyman says:

    denisdeng,

    You are welcome!

    Tino,

    I was thinking that you would be one to comment on this. 🙂

    I'll reply to the things you bring up in order:

    1) It totally depends on the web page you are using it in. However, compared to loading a JavaScript library and one or more other dependencies, especially since you never know which files will apply event handlers to the page, I personally find it to be a quick and small solution to catching the clicks.

    2. I agree, however if the delay between actual click and loading of JavaScript files and applying of event handling, there are some other performance issues as well with the web site.

    Adding a throbber or something isn't that bad, though. Just append an element to the document and position it next to the click or at a fixed position, and could easily be added to this script.

    3. It's a valid point, and just the first one I took for its small size – feel free to replace it with anyone of your liking.

  • Joey says:

    My concern is when there is a javascript error (and that does happen sometimes no matter how hard we try), you are left with a page that is unusable instead of a page that degrades into traditional interaction.

  • […] ????Catching Clicks With ClickCatcher Before Your JavaScript Files Have Loaded/Events Been Applied? ????Flexible Javascript Events???Firing Javascript Events (like onchange)? JavascriptJavascript, ??← CSS?????????? ????????????????? Name (*) […]

  • Robert Nyman says:

    Joey,

    Well, you need to work with feature detection and possibly try…catch clauses and similar to prevent that as far as you can.

  • Emil Stenström says:

    Thanks för a fun article! Always nice to see some of the code you guys are working on…

    One catch though… The addEvent script you're using relies on function hashes which are not implemented in at least mobile iPhone browsers… So use a better one!

  • Robert Nyman says:

    Emil,

    Thanks!

    And yes, I believe the addEvent approach here is a bit superfluous as well – it would do just fine with <code>attachEvent</code> and <code>addEventListener</code>. I will probably change that.

  • Ionut Popa says:

    Hello Robert,

    where do you call the "callClicks " function?

  • Robert Nyman says:

    Ionut,

    You could call it in the loading function, or make a manual call to it yourself when you know you have loaded all necessary resources.

Leave a Reply to Ionut Popa Cancel reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.