XPages namepicker using standard typeahead
A couple days ago, Julian Buss released a very cool namepicker for XPages. This article will show you how to enable a similar feature in your XPage applications using only the standard typeahead functionality.
Typically, when enabling typeahead for an Edit Box control, we populate the valueList attribute with the full list of options that should be selectable - @DbColumn or an equivalent. In other words, this expression evaluates to the list that should be searched for partial matches, and when the user starts typing, Domino filters that full list. There are (at least) two limitations to this approach:
- It doesn't scale. The typical approach to typeahead will fail when used as a namepicker for almost any organization, because there are simply too many people in the average Domino Directory. This is true of any large dataset, due both to the time it takes to query it, and to the inherent limitations of some methods of doing so.
- It's boring. I love that typeahead is so easy now, but in many cases, having additional contextual information for each suggested match would make the typeahead results far more useful to the end user.

The key to this particular technique is the mysterious valueMarkup attribute associated with the AJAX Type ahead control (in the Source, <xp:typeAhead/>). When you enable typeahead for any Edit Box control, it automatically adds an AJAX Type ahead control as a child element of that Edit Box. Some of its properties can be set directly from within the properties pane of the parent Edit Box, but (again, like so much of the functionality of XPages), some properties can only be set by navigating to the child control - either via the Outline or directly within the Source:

Once you've selected that control, you'll see a couple useful settings in the All Properties pane:

When valueMarkup is set to true, this tells Domino that you will handle all of the filtering yourself - in other words, instead of specifying an expression that evaluates to the entire dataset your users can choose from (in the case of a namepicker, the entire Domino Directory), your valueList attribute will be assumed by Domino to be an expression that returns only the matching results. So how do you actually do that filtering? Well, that's what the var attribute is for.
If you're familiar with repeat controls, you're already accustomed to defining a var attribute; in a repeat, that's where you specify what variable should be bound to each iterated member of the repeat. In the case of an AJAX Type ahead control, however, the var attribute designates the variable that will be bound, in each typeahead request, to what the user typed. Hence, in the above example, every time the valueList attribute is evaluated (namely, during every typeahead request), the variable named searchValue has a String value matching whatever characters the user has already entered. That String is passed as an argument to the directoryTypeahead function, which is contained in a script library I wrote for the purpose of this article... you could certainly define this functionality locally to each control, but for something this universal, it's obviously better to define it in a script library. Let's take a look at what that function does:
var directoryTypeahead = function (searchValue:string) {
// update the following line to point to your real directory
var directory:NotesDatabase = session.getDatabase("", "test/spnames.nsf");
var allUsers:NotesView = directory.getView("($Users)");
var matches = {};
var includeForm = {
Person: true,
Group: true
}
var matchingEntries:NotesViewEntryCollection = allUsers.getAllEntriesByKey(searchValue, false);
var entry:NotesViewEntry = matchingEntries.getFirstEntry();
var resultCount:int = 0;
while (entry != null) {
var matchDoc:NotesDocument = entry.getDocument();
var matchType:string = matchDoc.getItemValueString("Form");
if (includeForm[matchType]) { // ignore if not person or group
var fullName:string = matchDoc.getItemValue("FullName").elementAt(0);
if (!(matches[fullName])) { // skip if already stored
resultCount++;
var matchName:NotesName = session.createName(fullName);
matches[fullName] = {
cn: matchName.getCommon(),
photo: matchDoc.getItemValueString("photoUrl"),
job: matchDoc.getItemValueString("jobTitle"),
email: matchDoc.getItemValueString("internetAddress")
};
}
}
if (resultCount > 9) {
entry = null; // limit the results to first 10 found
} else {
entry = matchingEntries.getNextEntry(entry);
}
}
var returnList = "<ul>";
for (var matchName in matches) {
var match = matches[matchName];
var matchDetails:string =
"<li><table><tr><td><img class=\"avatar\" src=\"",
match.photo,
"\"/></td><td valign=\"top\"><p><strong>",
match.cn,
"</strong></p><p><span class=\"informal\">",
match.job,
"<br/>",
match.email,
"</span></p></td></tr></table></li>"
].join("");
returnList += matchDetails;
}
returnList += "</ul>";
return returnList;
}
Most of the above probably speaks for itself, but I want to specifically mention two basic concepts about the format of the value it returns:
- The return value for the valueList expression must be a <ul/>. The client-side JavaScript that processes typeahead requests is expecting to be sent the markup of an unordered list. When the valueMarkup attribute is omitted (or false), Domino does this automatically by filtering the valueList and formatting any matches as <li/>'s inside a <ul/>. Conversely, by setting valueMarkup to true, we've committed to doing both the filtering and the formatting ourselves, so the String we return must conform to the same format.
- Non-value text must be wrapped in a special span. This part's not very intuitive, but once you're aware of it, it's easy to format the results any way you like: when the user selects a value from the typeahead suggestions, Domino ignores any tags inside the corresponding <li/> and treats the combination of all "tagless" text runs as the selected value. In the above example, for instance, the first text it finds that is inside a tag - but not part of one - is the user's common name. Because we want the job and the email to appear in the typeahead suggestion, but we don't want it to be considered part of the selected value, it has to be wrapped inside a span with a class of "informal". This tells Domino to ignore anything inside that span, treating only the common name as the actual value the user is selecting. Yes, this is a weird convention, and there would have been no way of guessing it without IBM spelling it out for us... but now that they have, we can add all manner of useful information into our typeahead results without it injecting this additional information into the field when the user selects a suggestion.
You can try out a live demo of this technique, and of course the example database is also available for download.
Comments
Posted by Tim Tripcony At 01:09:43 AM On 11/01/2009 | - Website - |
Posted by Nathan T. Freeman At 12:29:44 AM On 11/01/2009 | - Website - |
Tx Tim.
Posted by Theo Heselmans At 06:43:36 AM On 11/01/2009 | - Website - |
Thanks for sharing this.
Posted by David Leedy At 07:04:50 AM On 11/01/2009 | - Website - |
Posted by Karsten Lehmann At 08:55:20 AM On 11/01/2009 | - Website - |
var directory:NotesDatabase = session.getDatabase("", "test/spnames.nsf");
Will point to the local machine if your running Xpages in the client instead of pointing to the server. You can resolve it by using database.server() instead of the the ""
Posted by Declan Lynch At 09:35:37 AM On 11/01/2009 | - Website - |
This is a great namepicker, as is Julian's. I can't wait to see how it evolves.
Posted by Declan Lynch At 09:31:46 AM On 11/01/2009 | - Website - |
Posted by Tim Tripcony At 10:05:18 AM On 11/01/2009 | - Website - |
Posted by Bruce Elgort At 10:52:00 AM On 11/01/2009 | - Website - |
Posted by Peter Presnell At 11:09:22 PM On 11/01/2009 | - Website - |
Posted by John Palmer At 09:08:42 AM On 11/02/2009 | - Website - |
I've also never seen anyone do string matching the way you did with includeForm, so I gotz me some bonus learnin', too.
Posted by John Smart At 12:08:46 AM On 11/06/2009 | - Website - |
Posted by Lou Gerritse At 01:12:10 PM On 08/26/2010 | - Website - |
Posted by Gordon Cuthbertson At 07:02:24 AM On 12/08/2010 | - Website - |
Thanks for sharing this tip!!!
Posted by S Banks At 11:30:32 AM On 03/23/2011 | - Website - |