Tooltips from Anchors

Often times users ask how to display a TooltipDialog from something other than a DropDownButton. Until recently, I’d not thought it to be possible. I have been wrong before.

For a long time, I’d held the belief there was some magical required relationship between the dijit.TooltipDialog and the dijit.form.DropDownButton — citing a11y requirements, or design of the Tooltip itself. I’d always wondered why you had to declare your Tooltip content inside the containing button, entirely eliminating the possibility of a degradable / unobtrusive experience. Something as simple as connecting a popup to a plain ole’ anchor tag should be possible! Turns out it is. Following along with the debunking of myths, I investigated some of my own doubts and entirely disproved myself. +1 Dijit.

First, start with an entirely working form and login link:

<html>
<head>	
	<style type="text/css">
 
		/* basic tooltip styles */
		#tt { width:275px; }
		#tt input {
			width:175px;
			border:1px solid #7EABCD;
			padding:4px;
			line-height:0.9em;
			margin:2px;
		}
 
		/* I'm cheating here. Using dojo to add '.dijitInline' which is
		   cross-browser display:inline-block; 
		*/
		#tt span {
			width:50px;
			text-align:right;
			padding-right:8px;
		}
 
		/* to help in degrading */
		#formArea { display:none; }
		.no-js #formArea {
			display:block;
		}
 
	</style>
 
</head>
<body class="tundra no-js">
	<h1>Hello, Dijit</h1>
 
	<!-- a link, which will open a popup if js is enabled -->
	<a class="loginLink" href="login.html">login</a>
 
	<!-- a plain form area -->
	<div id="formArea">
		<div id="tt">
			<form id="loginForm" action="login.php">
 
				<div>
					<label for="loginName"><span>Name: </span></label>
					<input type="text" id="loginName" name="loginName">
				</div>
 
				<div>
					<label for="loginPass"><span>Password: </span></label>
					<input type="password" name="loginPass" id="loginPass">
				</div>
 
				<div>
					<button id="submitter" type="submit">Login</button>
					<button id="cancel" type="reset">Cancel</button>
				</div>
 
			</form>
		</div>
	</div>
 
</body>
</html>

We’ve got a basic link to a page: login.html, and a form that will submit to login.php — let us assume those links both work, and accept use login information however you would on the back end.

We’ve added a weird combo to the class attribute of the body, to assist in our degradable efforts. We’ll use JavaScript to remove the class “no-js” from the <body>, and enable us to make classes to work with in our design in both “modes”: with and without javascript enabled. You can see this in action by the css rules:

#formArea { display:none; }
.no-js #formArea {
	display:block;
}

When the class .no-js exists, the form area is visible. Otherwise, hide it. We’ll be turning it into a tooltip progressively. We’ve added “tundra” because it is irrelevant in the non-javascript case, and useful in the javascript case.

Now, on to adding the Dijit widgets to the page. Add in the <head> element three things: a link tag to pull in the theme css, a script tag to pull in Dojo, and a script tag to pull in our code:

<!-- i have a hard time remembering the google CDN link. -->
<link rel="stylesheet" href="https://tinyurl.com/dojo-12-tundra" />	
<script src="https://tinyurl.com/dojo-12"></script>
 
<!-- load in our javascript -->
<script src="tooltip-anchor.js"></script>

Now, in the tooltip-anchor.js file, load in the TooltipDialog code after the page has rendered, and add another addOnLoad function where we’ll put our code. In between addOnLoad’s, we’ll remove the “no-js” class from the body element, as JavaScript it clearly enabled at this point.

// document is ready:
dojo.addOnLoad(function(){
 
	// remove our non-js class identifier
	dojo.removeClass(dojo.body(), "no-js");
 
	// load in the required resources. In Dojo 1.3, it will be
	// dojo.require("dijit.TooltipDialog"), though the following will still work:
	dojo.require("dijit.Dialog");
 
	// re-register, so Dialog code is ready: 
	dojo.addOnLoad(function(){
		// our code goes here
	});
 
});

So now we need to do a couple of things in the inner addOnload function. First, locate the login link, and run a forEach function passing the node as ‘n’:

dojo.query(".loginLink").forEach(function(n){
	// do more stuff for these (this) node: n
});

We cheat and implement Dijit’s cross-browser display:inline-block by adding a class to the spans in our tooltip node (this is all within the above forEach function):

	dojo.query("#tt span").addClass("dijitInline");

This isn’t required, and may effect your custom styles. It helps here to align the spans in this example, because they have widths but need to be inline …

Next, create a new Tooltip from the node with id=”tt”, storing a reference to the variable myDialog. We can pass custom TooltipDialog options here in the object, but the defaults will suffice for now:

	var myDialog = new dijit.TooltipDialog({}, "tt");
	myDialog.startup();

Now, connect to the plain anchor link (our ‘n’ reference in this loop) “onclick” event. This will show our TooltipDialog by calling dijit.popup.open(). Don’t forget to prevent the native anchor from following it’s link by calling preventDefault():

	dojo.connect(n, "onclick", function(e){
		// stop the native click 
		e.preventDefault();
		dijit.popup.open({
			popup: myDialog, around: n
		});
	});

We also want some want to hide the Tooltip at some point. This is where DropDownButton does a lot of hard work regarding focus / blur / positioning. In our simple case, our form actually has a “reset” button, so we’ll connect to it’s “onclick” and use that to close the popup:

dojo.query("#cancel").onclick(function(e){
	e.preventDefault(); // like return false; 
	dijit.popup.close(myDialog);
});

And last but not least, hooking into to the form’s native onsubmit event, and submitting the form via Ajax (don’t forget to prevent the native submit event from taking over):

dojo.query("#loginForm").onsubmit(function(e){
	e.preventDefault();
	dojo.xhrPost({ 
		form:"loginForm",
		handle: function(data){
			// inspect the response data:
			console.log(typeof data, data);
		}
	})
});

xhrPost is just one of many Ajax transports available. The important part here is either manipulating the final action=”" of the form by specifying a new url:”" parameter, or doing something on the server side to handle both the Ajax and native case. A special header “X-Requested-With” is sent along in the Ajax request, so you can easily detect the difference.

For instance, with the Zend Framework, you can use the isXmlHttpRequest() method:

	if($this->getRequest->isXmlHttpRequest()){
		// just send some data
	}else{
		// display the full view?
	}

Or perhaps with django:

def view(request): request.is_ajax()

Tags: ,