Heavily Customizing a Bootstrap Typeahead
Update 2013-03-20: I've updated most of the sections below with embedded working examples. You can try them out for yourself, and see the source code by clicking the "Source Code" link at the top of each box. These new embedded examples use different sample data than the original post. I went with James Bond actors and movies because it's a small dataset that was easy to work with, rather than the production dataset I was working with in my screenshots when I originally wrote this post. That dataset would have been hard to separate or recreate and doesn't really add anything.
In the future if/when I do embedded examples like these I'll try to make them consistent with the post, but hopefully these still prove useful.
What follows is a (long) chronicle of my adventures customizing a Bootstrap Typeahead control to fit my needs. I can't claim that this is an ordained methodology because the documentation is pretty scant on what exactly you can and can't do with them; but it works and my changes will be used in production very soon.
The most basic typeahead control is simply a filterable list of strings. For example if you want someone to choose their state of residence, you could populate an array with all 50 states and then as the user types they see a list of items that match what they've entered so far:
More complex...
Let's say that you're using this to select a person record from your database instead, and the name doesn't matter so much, it's the person's ID you're after. This is where the other methods come in. You've got to start getting a little bit clever.
Start by adding an additional hidden form field to hold the selected user's id value. I also recommend disabling autocomplete (for modern browsers, at least) so it doesn't compete for screen space with your typeahead:
<input type="text" id="user-input" autocomplete="off" /> <input type="hidden" name="userId" id="userId" value="" />
Then modify your JavaScript:
var users = {};
var userLabels = [];
$( "#user-input" ).typeahead({
source: function ( query, process ) {
//the "process" argument is a callback, expecting an array of values (strings) to display
//get the data to populate the typeahead (plus some)
//from your api, wherever that may be
$.get( '/api/users/search.json', { q: query }, function ( data ) {
//reset these containers
users = {};
userLabels = [];
//for each item returned, if the display name is already included
//(e.g. multiple "John Smith" records) then add a unique value to the end
//so that the user can tell them apart. Using underscore.js for a functional approach.
_.each( data, function( item, ix, list ){
if ( _.contains( users, item.label ) ){
item.label = item.label + ' #' + item.value;
}
//add the label to the display array
userLabels.push( item.label );
//also store a mapping to get from label back to ID
users[ item.label ] = item.value;
});
//return the display array
process( userLabels );
});
}
, updater: function (item) {
//save the id value into the hidden field
$( "#userId" ).val( users[ item ] );
//return the string you want to go into the textbox (e.g. name)
return item;
}
});
The source() function is responsible for taking the input from the text field, using it to get results to display, and passing those results to process(). Since we're going to be converting a string back to an ID once a record is selected, we save a hash-table with the name as the key (and yes, you can have spaces and other odd characters in an object key name) and the ID value as the key value. Then a simple array of names (['John Smith','Jane Smith',...]) is passed to the process() callback.
The updater()function is called when an item is selected, and the selected item's label is passed to it. This is where we convert "John Smith" back to his person ID. We store that person ID into the hidden form field, and then return the string that we want to be placed into the textbox; in this case the name of the person.
Now when the form is submitted, the hidden form field has the selected person's ID, and we can just ignore the name value from the textbox field.
Even More Complex...
One thing that would be nice if it were built into the typeahead control is automatic throttling of calling the source() method on keypress events. If you're hitting an API for the data, best practices would say that you should wait until the user stops typing for some fraction of a second before sending the API request. Otherwise you'll be sending one request per letter pressed.
There's a great method in the Underscore library for throttling events like this: debounce. Debounce waits until it's been N milliseconds since the last time the function was called to invoke it.
So we rewrite our source() method like this:
var users = {};
var userLabels = [];
$( "#user-input" ).typeahead({
source: function ( query, process ) {
searchPeople( query, process );
}
);
And we wrap its original contents up in a new function named searchPeople:
var searchPeople = _.debounce(function( query, process ){
$.get( '/api/users/search.json', { q: query }, function ( data ) {
users = {};
userLabels = [];
_.each( data, function( item, ix, list ){
if ( _.contains( users, item.label ) ){
item.label = item.label + ' #' + item.value;
}
userLabels.push( item.label );
users[ item.label ] = item.value;
});
process( userLabels );
}
}, 300);
The last line specifies that it should wait 300ms after the last invocation to run. This will cause our AJAX-based typeahead to send less requests to the server, and probably be faster overall as a result.
We Must Go Deeper...
What if you want to do a more advanced search, like allowing input "sm,j" to match partial last names, and partial first names, so that you can filter within the Smiths to show only those whose first name contains a J? Most of the work for this is in the API, but there's one part of Bootstrap worth mentioning here. By default bootstrap will hide any items from the dataset that don't contain an exact match of the textbox contents. So if you search for "sm,j" and both "Smith, John" and "Smith, Jane" are returned, neither will be included in the results; because neither contains the string "sm,j". What you need to do is add the matcher() method, and simply return true. This is safe because the server-side API has already filtered the results for us, so we want to display everything that the server returns:
$( "#user-input" ).typeahead({
matcher: function () { return true; }
);
Deeper Still
What if a simple string display isn't good enough? Sure, knowing there are two different John Smith's and being able to see the person ID will help you pick the right one; but only if you know the person ID. A better approach would be to show some additional metadata about each person so that it's easier to pick the one you want. If you show their department and the number of years with the company, that should be enough. Heck, if you have headshot photographs available, that would be perfect. But how do you fit all of that in?
Enter the highlighter() method. As documented, this method gives you the opportunity to change the HTML of each item in the list to show how/why it matches. The default implementation bolds the matching section of the string. You can see in the first example above that the "h" in Idaho has been bolded this way. A simple override might bold it and change the color so that it stands out more.
But you can also use this method to get a little crazy. You'd start by including the additional metadata in your API results, of course. Then you've got to save it just like our original hash table for name->id, but more complex:
_.each( data, function ( item, ix, list ) {
if (_.contains(recruits, item.label)){
item.label = item.label + ' #' + item.value;
}
users[ item.label ] = {
id: item.personId
,name: item.lastname + ', ' + item.firstname
,dept: item.dept
,photo: item.photoURL
};
userLabels.push( item.label );
});
Here, instead of just the ID as the value of each hash key, we store an object with all of the metadata about the person. Then, we use the highlighter() method to return some more useful HTML:
$( "#user-input" ).typeahead({
highlighter: function(item){
var p = users[ item ];
var itm = ''
+ "<div class='typeahead_wrapper'>"
+ "<img class='typeahead_photo' src='" + p.photo + "' />"
+ "<div class='typeahead_labels'>"
+ "<div class='typeahead_primary'>" + p.name + "</div>"
+ "<div class='typeahead_secondary'>" + p.dept + "</div>"
+ "</div>"
+ "</div>";
return itm;
}
});
Then you just have to add some CSS to align things, maybe make the primary text bold and the secondary text a little bit smaller...
.typeahead_wrapper { display: block; height: 30px; }
.typeahead_photo { float: left; max-width: 30px; max-height: 30px; margin-right: 5px; }
.typeahead_labels { float: left; height: 30px; }
.typeahead_primary { font-weight: bold; }
.typeahead_secondary { font-size: .8em; margin-top: -5px; }
And the end result could look something like:

Bootrapception
Now let's really get crazy. What if you want to use your typeahead control within a Bootstrap modal? In general, there's nothing wrong with this and it works exactly the same as previously. The only caveat is that the unordered list that's created for your typeahead dropdown is part of the .modal-body section, and therefore if it is longer than the contents of the modal, it hides behind the modal footer and you've got to scroll the contents of the modal to see it.

You may be fine with this behavior. I wasn't; particularly because when you hit the down arrow key to select a different item from the list, and your selection isn't visible, it appears as if nothing happens. I'm sure that the list-item has a blue background, but it's not visible so it's not helping much. I wanted my typeahead list to expand beyond the body of the modal.
Unfortunately, because of the way Bootstrap modal dialogs are placed, this wasn't a straight CSS fix. The only way I've found to get the UL to display on top of the modal footer is to use position:fixed which requires precise top & left values; but modals are not absolutely positioned. It's not ideal, but it can be done with JavaScript without putting too much strain on the browser.
First I added a class to the div that wraps the label, textbox, and the UL that's generated for the typeahead (the .controls div from my form). Then I added some CSS to force the UL to use position:fixed, and while we're at it, always be the same width as the textbox:
.recruitTypeahead ul {
width: 350px;
position: fixed;
}
But as I said, we need the precise top & left values to position it correctly, and this can only be found with JavaScript. So we create a function to calculate the position of any element on the page:
function elPos(element){
var position = { x:element.offsetLeft, y:element.offsetTop };
while(element = element.offsetParent){
position.x += element.offsetLeft;
position.y += element.offsetTop;
};
return position;
}
And then use it to find the position of the textbox. After a little bit of playing with it I decided that the textbox's top + 32 pixels was a good value for the UL's top, and I give them the same left value. (Note that hard coding the value like this comes with its own problems: What if the user's browser has the font size cranked up? But it's close enough to get us started. If you want to take it further, use the object's height, plus a small amount of padding.)
var fixPersonTypeaheadPos = function(){
var box = $("#user-input")[0];
var newPos = elPos(box);
$(box).parent().find("ul.typeahead").css({top: newPos.y+32, left: newPos.x});
};
It seems like Bootstrap updates the position of the UL (or more likely deletes and recreates it), every time the typeahead content changes, so you've got to call this method after the content has been supplied, every time it changes. Unfortunately Bootstrap doesn't expose an event for this--but we can piggyback on an existing event.
You could use either the highlighter() method or the matcher() method. I chose matcher() because my matcher implementation is far less complex, so it will run more rapidly. But still, it ends up calling the method once for each item in the dropdown, which would be overkill for our purposes. Our first thought should be to use debounce to prevent unnecessary invocations, and that's not wrong, but this is also a good opportunity to use debounce's immediate argument, so that the re-positioning is done as quickly as possible but subsequent rapid calls to the method do nothing:
$( "#user-input" ).typeahead({
matcher: function(item){
fixPersonTypeaheadPos();
return true;
}
});
var fixPersonTypeaheadPos = _.debounce(function(){
var box = $("#user-input")[0];
var newPos = elPos(box);
$(box).parent().find("ul.typeahead").css({top: newPos.y+32, left: newPos.x});
}
, 10, true);
This runs once immediately, and then won't execute again until it's been at least 10ms between invocations; and since each iteration of the function is simply returning true, it should only be 1 or 2ms between invocations, and so only the first invocation causes the function to execute.
Put it all together and we've achieved the desired result: the typeahead dropdown menu expands on top of the modal footer and is completely visible.

Published 2013-01-23 11:30 in 24 Responses JavaScript



This November I'm running a marathon to raise money for Children's Miracle Network. But instead of running a marathon I'll be playing an epic 24 hour video game marathon.
24 responses:
Alex
Mahbub
Adam
Mahbub, I'll see if I can separate it out from what I'm working on and toss it out there. It'll take some time, though.
Louie
Tony Dew
Alec E
Question out of left field: I'm working on a project that I'm already using Bootstrap extensively for and need to have a typeahead function like this. However I need it to dump the results from the search into a DIV (specifically, a list of checkboxes with labels) and not a dropdown. Any thoughts on how one would accomplish this?
Thanks again for all the info!
Adam
PJ
fyi: regarding section "More Complex", the line:
data = $.parseJSON(data);
is in the web page, while not in the source code.
lol, lost some hair troubleshooting that one. :)
Adam
Arif Majid
have been using jquery autocomplete in bootstrap themes ... now finally something great to work with.
Thanks
PJ
Interesting you should say that.
I'm returning data from an mvc jsonResult.
For me:
Without the parseJSON, as I stepped through the method, each iteration had only on character of the data string, and things didn't work. It stuck at the process() line per firebug.
With parseJSON, as I stepped through, each iteration had the full Bond name, and worked perfectly.
So maybe it's an implementation situation with my application.
All I know, it's working now with parseJSON in the code.
Thanks again
PJ
May I suggest that you keep it in the code, but commented out.
Include a note like "This may be necessary for proper execution."
This way both situations are covered.
Adam
Will
daniel
Manifiestate.net
We will use in our future version the extended "highlighter" with image you've posted.
Javier
Brig Lamroeaux
Yuri
Nevertheless, there is still an annoying and disappointing thing I've been struggle for couple of days with no luck.
Imagine that a user types in something without selecting an item from the list. (Actually, you can try it on the second or third form on this page). Let's say he types "Timothy Dalton", then clicks outside the typeahead field (imaginary submit button for example or other field in the form). In this case the hidden field value remains EMPTY :( Or, try to input any other correct name (Daniel Craig), select it, hidden field is set to actor's id = 8, then you change your mind, erase Craig and input somebody else's name or any other "bla-bla".. Click outside or "submit". Id remains unchanged and it's still Craig's 8.
It's a pity but it makes the pretty hidden field id passing solution useless if we're going to save it into DB for example.
I would greatly appreciate any suggestions about how to get this great typeahead feature to work properly with item id passing.
Adam
I think the problem you describe is just a shortcoming of the way typeaheads work. This flaw is inherent even in the most basic implementation where everything is hard-coded. But of course there's always a way around it.
For starters, you could update the highlighter or source method (no point in doing both) to clear out the hidden form field, because anything that triggers them is essentially a further search. You could also always update the hidden form field to be the first value in the search results; which is *sort of* what's implied by clicking elsewhere on the page to make the typeahead go away, since the first item is highlighted by default.
My last idea is to build on the first: clear the hidden field in the source function, and then on blur of the text field, if the hidden field is still empty, also clear the text from the typeahead, to give the user feedback that they haven't actually selected anything.
Jashk
Paul Stocks
Is it possible to trigger the Typeahead from the updater?
I'd like to 'page' long lists with a '-- more --' at the bottom.
I can reset the original query but can't trigger a change event.
P:)
Kevin Jensen
Matt Bram
I've been trying to figure out the highlighter function for a couple of hours and I immediately saw what I was doing wrong. Thanks!
Your comment: