Attaching Click Events On Demand From Blur Events

Todays article dicusses a problem I have been encountering when assigning on demand click events to the document from a blur event. The problem arrises as the click and blur events have no gauranteed order of firing, and because the click event is attached to the document, it is easily triggered. Suppose the following problem: when a blur event fires you want to show a tooltip message that should disappear on a click event, so you attach the click event listener to the document in response to blur event. However, the user clicks out of the input element triggering both a click event and the blur event. Most of the time, in most browsers, the blur event fires before the click event finishes bubbling, so the click event on the document fires as soon as it is attached.

The Problem

Here is an example of how to setup the issue:

YUI().use(node, function(Y) {
	var tooltipNode = new Y.get(#myTooltipElement);

	// this is the click event that we attach to the document
	var clickCallback = function() {
		docNode.detach(click, clickCallback);
		tooltipNode.setStyle(display, none);
	};
	
	// this is the triggering callback
	var blurCallback = function() {
		var docNode = new Y.Node(document);
		docNode.on(click, clickCallback);
		tooltipNode.setStyle(display, block);
	};

	Y.get(#anInputElement).on(blur, blurCallback);
});

Solution 1: Ignore The First Click

The first possible solution, is to ignore the first click after attaching the document click listener:

YUI().use(node, function(Y) {
	var tooltipNode = new Y.get(#myTooltipElement);
	var clickCount = 0;

	// this is the click event that we attach to the document
	var clickCallback = function() {
		if (clickCount) {
			docNode.detach(click, clickCallback);
			tooltipNode.setStyle(display, none);
		}
		
		clickCount += 1;
	};
	
	// this is the triggering callback
	var blurCallback = function() {
		clickCount = 0;
		var docNode = new Y.Node(document);
		docNode.on(click, clickCallback);
		tooltipNode.setStyle(display, block);
	};

	Y.get(#anInputElement).on(blur, blurCallback);
});

This is a simple solution and works, but is a bandaid for the real problem, and may lead to undesirable user experience, where end-users have a difficult time understand how to clear the tooltip.

Solution 2: Using A Timeout To Attach The Click Listener

The second possible solution, is to use a timeout to attach the document click listener:

YUI().use(node, function(Y) {
	var tooltipNode = new Y.get(#myTooltipElement);

	// this is the click event that we attach to the document
	var clickCallback = function() {
		docNode.detach(click, clickCallback);
		tooltipNode.setStyle(display, none);
	};
	
	// this is the triggering callback
	var blurCallback = function() {
		var docNode = new Y.Node(document);
		setTimeout(500, function() {
			docNode.on(click, clickCallback);
		});
		tooltipNode.setStyle(display, block);
	};

	Y.get(#anInputElement).on(blur, blurCallback);
});

This also works, but is just another bandaid for the real problem, and cannot gaurantee that it will work correctly. By waiting a small amount of time we allow the browser to finish handling the blur event and any related events, before attaching the click event listener. After some experimentation, I learned that most browsers, on average machines, will finish processing the events in less than 450ms, so I choose 500 milliseconds.

Conclusion

I have not found a perfect solution for attaching click events to the document in response to a blur event. Each browser handles bubbling and the order of event firing slightly differently, so a solution cannot depend on event order. In my own code, I have been using the second example, because it works correctly most of the time. I would be interested in hearing if anyone has a better solution.