- Settings:
Page
Normal
Plain layout without fancy styles
Font
Normal
Large fonts
Very large fonts
Colour
Normal
Restful colour scheme
Light text on a dark background

Note: user account creation on this site has been disabled.

Programming, speculative fiction, science, technology
Powered by Drupal, an open source content management system
Scope
Include Children
If you select a term with children (sub-terms), do you want those child terms automatically included in the search? This requires that "Items containing" be "any."
Categories

Everything in the Events vocabulary

"Events" is used for: Convention Post, Page, Story.

People mentioned in the articles

"People" is used for: Page, Convention Post, Story.

Themes mentioned in the article

"Themes" is used for: Page, Convention Post, Story.
Skip to top of page

Customizing Angular tooltips with your own directive

Tooltips are a useful thing. Just last year, as a volunteer web developer, I built an event website for a nonprofit. The website had a page with the event program grid. The event had several parallel tracks, each of them jam-packed of back-to-back panels, and each panel had several panelists. Understandably, the web page real estate was at a premium, and the page for the panel grid listed the participants by only their last name. No other info.

If you are attending the convention and wondering what panel to go to next, you might want to know who the panelists on it are, because that makes a big difference in whether a panel is worth attending. Unfortunately, for that you would have to go to the page of panelists' bios, which means "losing your place" in the grid page. If only you could look up at least the very basics about the panelists without leaving the grid page!

Luckily, modern internet is capable of such miracles. We'll put panelist information in a tooltip, to be brought up with a mouse hover (in a desktop-based web browser) or with a click (in a mobile browser) on a panelist's name.

Since laziness is the greatest virtue of a developer, we don't want to write our own tooltip code. We'll use what other people have written. And since we are using Angular.js for the convention website, we'll use tooltips that were implemented in Angular. There happens to be such a handy project, Angular Tooltips by 720kb. It provides a directive, tooltips that's used as an attribute of an element for which you want to provide a tooltip. One of the arguments you can pass to it is tooltip-template="". To customize the tooltip for each panelist, you can pass as the template the name of a function that would retrieve a panelist's short bio. For example:


<span tooltips tooltip-template="{{ participantInfo(participants.foo) }}">Foo1 Foo</span>

Here, participants is an object in this page's controller that is basically a dictionary of participants. participantInfo is a function in the same controller that returns a small snippet of the bio, just enough to fit in a tooltip -- say, the first 15 words.


angular
  .module('app')
.controller('mainController' , mainController);

function mainController($scope, $location, $routeParams, $http) { 

    // shortname and linkid will usually be the same, except when there are 2 panelists with the same last name.
    // Then their linkids will be, e.g. a_foo, and b_foo, while their shortnames will be A. Foo and B. Foo
    $scope.panelists = {
      "foo": {fullname:'Foo1 Foo', linkid: "Foo", shortname: "Foo", website: "foo.com", 
	      bio: "Foo is the author of several books, including ****, ****, and ****, 
		   though she is better known as the author of more than 1,100 short stories published in 
		   ****, **** and ****."},
      "bar": {fullname:'Bar1 Bar', linkid: "Bar", shortname: "Bar", website: "bar.com", 
	      bio: "Bar was a writer on a variety of video game titles, including ****, ****, 
		   and ****. He is also the creator and author of the fantasy role-playing game ****."}
      }

      $scope.bioBeginning = function(person) {
	  if (person.bio) {
	    var bioWords = person.bio.split(" ");
	    var minBioLength = 0;
	    if (bioWords.length >= 15)
	      minBioLength = 15;
	    else 
	      minBioLength = bioWords.length;
	    var minBio = bioWords.slice(0, minBioLength);
	    return minBio.join(" ") + '... <a href="#/guest_bios#' + person.linkid + '">Read More</a>';
	  }
	  return "";
      }

      $scope.participantInfo = function(person) {
	  return "<div>" + person.fullname + "</div><hr/>" + $scope.bioBeginning(person) + "</div>";
      }

The tooltips now show up and are populated with the panelist's info.

Now we need to think what mouse event(s) should cause the tooltip to show up, and what event(s) should cause it to hide. The Angular Tooltips directive lets you specify a set of events for showing and for hiding the tooltip with tooltip-show-trigger="" and tooltip-hide-trigger="" arguments. For desktop-based browsers it is easy: we want to show the tooltip on mouseover (hover) and hide it on mouseout. But what about the mobile browsers? Since there is no such thing as hovering on a mobile browser, we want to show the tooltip on click. But... we also want to hide it on click. If the user clicks anywhere else on the page, the tooltip should go away. The problem is, if we specify it like this:

tooltip-show-trigger="click" tooltip-hide-trigger="click"

the tooltip will never show up, because the same click event that showed it will also hide it.

Turns out, the touch-based event touchstart will work fine to show the tooltip. Granted, if the user wants the tooltip to stay up, instead of going away immediately, they'll have to press the screen for a myriadth of a second longer. But they'll get the hang of it. Meanwhile, the click event can still be used to hide the tooltip.

Combining those events with mouseover and mouseout, we get this:


<span tooltips tooltip-template="{{ participantInfo(participants.foo) }}" tooltip-show-trigger="mouseover touchstart"
tooltip-hide-trigger="mouseout click" >Foo1 Foo</span>

We probably also want an "x" in the tooltip to close it. That's also an option provided by Angular Tooltips.


<span tooltips tooltip-template="{{ participantInfo(participants.foo) }}" tooltip-show-trigger="mouseover touchstart"
tooltip-hide-trigger="mouseout click" tooltip-close-button="true" >Foo1 Foo</span>

Looks like our span tag is pretty bloated now. That's a lot of verbiage to copy and paste around every element. Maybe we could automate a lot of this?

Putting parameters in a call to .config()

Sure enough, Angular Tooltips provide some configuration options for some of this stuff that we want every tooltip to have, such as events for showing and hiding it, and the Close button. When we are defining our main module, the app module (which of course can be called anything we want), we can configure the tooltips with a call to config():


angular.module('app', [
		    '720kb.tooltips'
		    // , anything else we want to inject
])
.config(['tooltipsConfProvider', function configConf(tooltipsConfProvider) {
  tooltipsConfProvider.configure({
    'showTrigger': "mouseover touchstart",
    'hideTrigger': "mouseout click",
	'closeButton': true
  });
}])

And the span element can now be much smaller:


<span tooltips tooltip-template="{{ participantInfo(participants.foo) }}">Foo1 Foo</span>

A wrapper directive to reduce redundancy

We can still automate it further, because "Foo1 Foo" is the fullname field of participants.foo object. In fact, we can write a whole new directive to consolidate this redundancy. To make the name short, we can call this directive p-det. The functions for getting the tooltip content that previously belonged to the page controller can belong to this directive instead, which is the proper place for them.

angular.module('app').directive('pDet', function() {
    return {
	   restrict: 'E',
	   scope: {
		  person: '=person'
		  },
	   templateUrl: 'templates/pDet.html',

	   link: function($scope, element, attrs, controller, transcludeFn) {
		 $scope.bioBeginning = function(person) {
			if (person.bio) {
			   var bioWords = person.bio.split(" ");
			   var minBioLength = 0;
			   if (bioWords.length >= 15)
			      minBioLength = 15;
			   else 
			      minBioLength = bioWords.length;
			   var minBio = bioWords.slice(0, minBioLength);
			   return minBio.join(" ") + '... <a href="#/guest_bios#' + person.linkid + '">Read More</a>';
		        }
			return "";
		}

		$scope.participantInfo = function(person) {
			return "<div>" + person.fullname + "</div><hr/>" + $scope.bioBeginning(person) + "</div>";
		}
	   } // end of link function
    } // end of return
});

As we see, it will have an argument person. This will be the same type of object as participants.foo.

We are putting the template for this directive in a separate file templates/pDet.html, in a templates directory relative to the application's root directory (of course, it doesn't have to be located there, but wherever makes sense in our application's directory structure). The template will look like this:

<span class="dotted-underline" tooltips tooltip-template="{{ participantInfo(person) }}">{{person.shortname}}</span>

And now an element for each guest in the web page will look like this:

<p-det person="participants.foo"></p-det>

You can automate it further, for example, by having the whole grid be generated by Angular.js from the participants object. Or so you think. But then you load the page, and a bunch of curly braces flashes before your eyes, because now Angular is rendering not just the tooltips but the panelists' names too. Suddenly you remember how flaky WiFi is at the hotel where the event takes place, and how hard it is to get a decent cell phone connection; what if over a slow network Angular will take forever to download to the attendees phones, and they'll be stuck with a pageful of curly braces? Not only they won't get any helpful tooltips, they won't even see the panelists' names. There must be a way around this, you think. If you can't enhance the user's experience, you should at least not ruin it.

(This is not really relevant in the context of the website I'm using as an example, because it already relies on Angular for navigation. Without Angular it wouldn't work at all. But if your website can work without Angular, then maybe you don't want to make some pages entirely dependent on it.)

I don't want a pageful of curly braces

You decide that instead of that you will leave the participant names hardcoded on the page. After all, the page is created not by you but by one of the event organizers, who puts together the program in an Excel spreadsheet and exports it as HTML. It has all the names in it already. What you'll do is to create a directive that will transclude those hardcoded strings, participant names. That way even with a slow, unreliable connection people will be able to look up the program grid and see panelist names, even if they won't be able to look up more information about them.

Tooltips wrapper with transclusion

Now we'll create a directive with transclusion. Let's call it p-info.

angular
.module('app')
.directive('pInfo', function() {
  return {
	 scope: { 
		person: '=person'
		},
	restrict: 'E',
	transclude: true,
	templateUrl: 'templates/pInfo.html',
	link: function($scope, element, attrs, controller, transcludeFn) {

		 $scope.bioBeginning = function(person) {
			if (person.bio) {
			   var bioWords = person.bio.split(" ");
			   var minBioLength = 0;
			   if (bioWords.length >= 15)
			      minBioLength = 15;
			   else 
			      minBioLength = bioWords.length;
			   var minBio = bioWords.slice(0, minBioLength);
			   return minBio.join(" ") + '... <a href="#/guest_bios#' + person.linkid + '">Read More</a>';
		        }
			return "";
		}

		$scope.participantInfo = function(person) {
			return "<div>" + person.fullname + "</div><hr/>" + $scope.bioBeginning(person) + "</div>";
		}
	} // end of link function
  } // end of return
});

In other words, the Javascript code for it is the same as for the p-det directive, except of course the template name, and the addition of transclude: true. But the template will be somewhat different.

<span><span class="dotted-underline" tooltips tooltip-template="{{ participantInfo(person) }}"><ng-transclude></ng-transclude></span></span>

Why two <span> elements? Because otherwise Angular will throw an error that there are multiple templates asking for transclusion.

The <transclude> element shows where to put the person's name, which is a hard-coded string on the page. The element for each guest will now look like this:

<p-info person="participants.foo">Foo</p-info>

As a side note, class="dotted-underline" is this:

.dotted-underline {    
  border-bottom: 1px dotted #000;
  text-decoration: none;
  cursor: default;
}

and all it does is put a faint underline under the words that are tooltip'able, to indicate to the user that the is something there is you hover or click on the word. Also it causes the cursor to remain the default arrow, instead of changing into a text cursor.

Another side note. The way the tooltip is displayed by default, it is high enough above the word that if you try to put the mouse over it (let's say you want to click "Read more"), the mouse will move off of the word, and the tooltip will go away. To cause it to be displayed lower above the word, I had to make a slight modification to angular-tooltips.css. In the CSS style tooltip._top tip I made changed:

top: -9px;

to

top: 0px;

Yes, angular-tooltips code has tooltip-class option where you should be able to assign your own class to the tooltip, but that didn't work for me.

And now the intrepid web developer needs to find some place with poor mobile signal, clear the phone browser's cache, and compare the two directives -- the one with transclusion and the one generates the page from scratch, and see if the version of the page that generates everything from scratch will actually have trouble loading. But with our luck, we know what will happen: we'll never find conclusive evidence. But at least we will have learned something new.