Disclaimer and License

Opinions expressed here by Tim Tripcony are his own and not representative of his employer.

Creative Commons License
Tip of the Iceberg is licensed under a Creative Commons Attribution 3.0 Unported License.
Based on a work at timtripcony.com.

Unless otherwise explicitly specified, all code samples and downloads are copyright Tim Tripcony and licensed under Apache License 2.0.

Search

« automatic recompilation - a great idea in theory but not in practice | Main| XPages safety tip - avoid using hyphens in control ID names »

XPages namepicker using standard typeahead

Category xpages
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:
  1. 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.
  2. 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 good news is that, like so much of what can be done with XPages, there's an easy (and, until recently, undocumented) way to push this feature to the next level, allowing you to add whatever content (and style) you like to your typeahead results. Before I explain this in detail, let's skip straight to dessert:



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:
  1. 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.
  2. 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.
There are, of course, a couple other things you'll want to keep in mind. For instance, since we're doing all the filtering ourselves, attributes like ignoreCase and maxValues are ignored, so you may want to ensure that the logic you're using to return the list handles any case sensitivity issues you may have, and you'll almost certainly want to include your own limit to keep it from returning everything that matches (the above example returns a maximum of 10 results). Finally, while the primary limitation of the default typeahead behavior is that it doesn't scale, using this manual typeahead filtering doesn't automatically provide infinite scalability. Implementing the above code as a namepicker for an enormous directory will work better than the standard @DbColumn approach (because at least it won't fail), but the results will be slow simply because of how long the getAllEntriesByKey call takes, especially if you have a low minimum character count (imagine, for example, searching ($Users) for everything starting with c: that's right, it'll return an entry for every document in the view, because there's a canonicalized entry for each in addition to all the other permutations). We've actually implemented a far more robust version of this which not only leverages the Directory API to ensure the results include any matching entries from cascaded address books and LDAP directories in addition to the standard address book, but also provides intelligent sorting of the results and takes advantage of additional features of XPages to provide maximum scalability. Naturally, I haven't been given permission to disclose the source of that beast... but don't worry, here at Lotus 911 we only use our powers for good.

You can try out a live demo of this technique, and of course the example database is also available for download.

Comments

Gravatar Image1 - I'm looking for a way to fetch value from Notes Local NAB suing xPages.Is it possible to retrieve names from Local NAB??

Gravatar Image2 - Holy Crap that's a great blog post!!!!!

Thanks for sharing this.


Gravatar Image3 - Holy sh** batman. You're gonna have me working late all this week as I try to impress a few of my clients with this. Awesome stuff Tim.

Gravatar Image4 - Well done Tim. Thank you for sharing.

Gravatar Image6 - Excellent. Thanks very much for the tip. I got it working with your example now to get it into my app which is causing some issue, but I will get it working.

Gravatar Image7 - Just stumbled of this, got it working fine.

Looks like the value returned into the exditbox is the value in the view which is ofcourse fine.

however, I do not want to fill the editbox with the value when the user cliked the dropdown, but instead redirect the user to the document clicked when the user click the dropdown entry.

in other words, I need to override click entry of the li elements and redirect the user to the selected unid instead of filling up the edit box.

any ideas how to do this?

Thanks
Thomas



Emoticon Emoticon Emoticon Emoticon Emoticon Emoticon Emoticon Emoticon Emoticon Emoticon

Gravatar Image8 - Thanks for the post, very interesting. Have you found anyway to trigger an event (such as an instant search) when you click on the Type Ahead entry rather than it just entering the text in the non-'informal' class span tags into the input box.

Gravatar Image10 - BTW this line :

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 ""

Gravatar Image11 - Excellent piece of code. Thanks for sharing this..

-Jay

Gravatar Image12 - Great stuff, Tim! Thanks!

I've also never seen anyone do string matching the way you did with includeForm, so I gotz me some bonus learnin', too. Emoticon

Gravatar Image13 - Fantastic, Tim! Have to jump right into designer to see what other useful stuff is there in the TypeAhead properties. Very cool!

Gravatar Image14 - Great solution, except I haven't been successful getting the selected entry returned to the input box other than by navigating to (using the arrow keys) or highlighting the entry, and hitting Enter. Shouldn't it work by selecting it with the mouse? Any ideas as to what I may have done wrong?

Thanks for sharing this tip!!!

Gravatar Image15 - Thanks for sharing! Very cool.

Gravatar Image16 - @7 - Good point, Dec. Just for clarification, the method call would be database.getServer().

Gravatar Image17 - Rather late, but I have a solution to Thomas's problem above (comment 16). I put all the text that's useful to the user in the 'informal' span, then added the UniversalId of the matchDoc, with font size 0, in a separate span afterwards, like this:

"<span style=\"font-size:0px\">" + match.unid + "</span>"

Then I use the onChange event to handle the rest of my processing which ends up with the typeahead field being cleared, so the user never gets to see the UNID that is returned.

Hope this saves someone the 6 hours it just took before such a simple solution dawned on me! Emoticon
Phil

Gravatar Image18 - great!!! coming a bit late from my side.. but REALLY great..

I don't know why everyone thinks a dialogbox name picker is so cool when you can have it the easy way!

we use type ahead to find airports and this technique to display the country flag in which the airport is located.. really really really nice!!

Gravatar Image19 - @Nathan - <xc:impulse loaded="false" />

Gravatar Image20 - Just couldn't stand it, could ya? Emoticon

Gravatar Image21 - I'm looking for a way to fetch value from Notes Local NAB suing xPages.Is it possible to retrieve names from Local NAB??

Gravatar Image22 - Hi Tim, How are you?

Gravatar Image23 - Just excellent, very cool.
Tx Tim.

Gravatar Image24 - As i said in the recent xPages podcast for Taking Notes : "Whoever creates the best namepicker will have a path beaten to their door."

This is a great namepicker, as is Julian's. I can't wait to see how it evolves.