Monday, October 14, 2013

Entering Recipients from an Address Book using jQueryUI MultiSearch

This might be one of the most common use cases (apart from entering tags) for this type of widget. Compose an email and start typing someone's name. Yahoo, GMail, all of them will offer suggestions from your address book. Select someone and start typing the next one. You can very quickly build a list of recipients using this UI pattern. GMail goes a step further by enhancing the experience - when you hover over an entered contact, you get a popover box with more details on the contact (a picture, Google+ circles, etc). Creating this type of user experience was one of the design goals I had when working on the MultiSearch widget. While the widget won't just provide all of this functionality, it does make it pretty easy to build.

Overview

Besides wiring up the source data, there's really only two parts you, as the developer, need to do to make this kind of UI work. The MultiSearch widget will handle the typing, searching, and rendering. You need to provide the markup and styling plus a little validation logic. If you're interested in a popover box, you'll need to define that content and some control logic. Here's a summary of the features we're going to build:

  • Searching - As the user types in the contact, we're probably going to search both the name and email. Returned results need to be rendered into a suggestion box. The content should probably include both fields so the user can see what they're matching and pick accordingly.
  • Selected - When an contact is selected, we need to render something into the list of recipients. This can be styled in several ways and probably should account for a contact not found in the address book. If its not found, we should probably ensure its a valid email.
  • Details - Hovering over the list of entered recipients triggers an event that we can render a popover. So far, the widget has been rendering everything for us. However, the only thing the widget provides here is a item select event with the specifics about the contact under the mouse. We'll need to manage the popover and rendering its content based on the the context provided in that event.


Let's walk through each feature and see how it can be implemented. If you want to skip the details and tinker with demo, its one of the examples on the project pages. A link is included to jump to the full source code. If you'd like a break down of the specific areas of functionality, keep reading.

Setup

Before diving in too far, we need some data and the base markup for the widget. For the data, I created a small 200 record local dataset where each record has the following definition:

   localData = [
         { 
           "id": 1, 
           "display_name": "Neal, Amelia R.",
           "organization":"XYZ Company",
           "primary_email":"pede@nibh.com",
           "primary_phone":"(577) 324-9152"
         },
  ...
];


And for the markup, I used the Twitter Bootstrap framework to structure my layout and styling for each widget box:



Finally, we can target the container DIV and initialize the widget. For now, I'll only include the data and attributes of interest:

   $("#recipients").multisearch( {
      source: localData,

      keyAttrs: [ 'id' ],
      searchAttrs: [ 'display_name', 'primary_email' ],
      
      ...
    });


As text is entered into the input box, its used to search localData in both the "display_name" or "primary_email". The field "id" is the default keyAttrs, but I set it anyways to point out its importance in properly trigger the notfound flag later.

Searching

Next, we need to create markup that will be rendered for each result found in the search. Since the search looks at both the "display_name" and "primary_email", both fields are included in the template:



Once the content is defined, we need to tell MultiSearch to use it by compiling it with Underscore's template function and setting the formatPickerItem option when initializing the widget:

   $("#recipients").multisearch( {
      ...

      formatPickerItem: _.template( $('#contact-item').html() ),

      ...
   });


Selected

Once a search performed, one of the results can be selected to add to the list of recipients. The markup template for this includes the "display_name" and an element to enable removing it after being selected:



Again, this needs to be compiled and set as the formatSelectedItem option when initializing the widget:

   $("#recipients").multisearch( {
      ...

      formatSelectedItem: _.template( $('#selected-item').html() ),
  
      ...
   });


So far, we've provided very basic options to the widget. Next, we'll add some logic to handle validating a contact that is not in the address book before allowing it to be added to the selected item list.

MultiSearch will allow anything to be entered as an item if the preventNotFound option is false (which is the default). In the case of entering recipients, we're ultimately interested in the contact's email address. If you pick someone from the address book, you know that the email is present. However, if the contact is not in the address book, you want to ensure a valid email address is entered. To perform this validation, we need to implement two callbacks:

   $("#recipients").multisearch( {
      ...

      buildNewItem: function( text ) {
         return { id: null, display_name: text, organization: '', primary_phone: '', primary_email: text };
      },

      adding: function( event, ui ) {
         var validater = new RegExp('^(?:[^,]+@[^,/]+\.[^,/]+|)$');

         $( this ).find( 'input' ).removeClass( 'error' );
         if ( ui.notfound ) {
            if ( !validater.test( ui.data.primary_email ) ) {
               $("#recipients").find( 'input' ).addClass( 'error' );
               return false;
            }
         }
      },

      ...
   });


First, buildNewItem allows us to define a record for an item that is not found in the search. This is called before any other events are triggered. We'll set both the "display_name" and "primary_email" to the entered text since we're using the "display_name" to render the content in the selected item list and will eventually be relying on the email address to be available when we query the widget for the list of entries. Additionally, to trigger the not found condition, and the "id" field needs to be "null". Next, we'll add a handler to the adding event such that we can validate the email address when the contact is not found in the search. The object we build in buildNewItem will be available as the "data" key in the second object passed to the handler. Since we created that object with the "primary_email" set to the current input text, we can validate it against a regex that validates email addresses. If invalid, we need to return false to cancel adding the item. Additionally, we can create a visual cue to alert the user the entry is currently invalid. For simplicity, I just added a error class to the input to provide feedback that the email is not valid.

Details

A final enhancement to the address list is to enable a popover box with more details about our selected contacts. In Gmail this box contains an image, other contact information, and Google+ circle membership. MultiSearch doesn't have a built-in feature to render a popover. However, it does generate several events we can tap into to make our own. In this example, I chose to add a handler to the itemselect event which is triggered when an item is selected via a mouse click or the available keyboard navigation (arrows + spacebar). We'll build an Underscore template just like the picker and selection template which will use the data passed to the handler:



Next, we'll compile that HTML into a variable we can use in the handler. The itemselect event provides both the element selected and the data that represents the item. It will be our responsibility to manage this content. First, we'll create the box and add it after the widget container in the DOM, then we'll position it under the selected item, animate it in use Bootstrap's CSS transitions, and, finally, listen for a click event on the document which signals the user clicked outside the bounds of the popover:


   var infoBox = _.template( $('#contact-info').html() );

   $("#recipients").multisearch( {
      ...

      itemselect: function( event, ui ) {
         
         // Generate from template and add to DOM
         var $info = $( infoBox( ui.data ) ).insertAfter( $( this ) ).show();
         
         // Use jQueryUI Position utility to move it to the right spot
         $info.position({
            my: 'center top+10',
            at: 'center bottom',
            of: ui.element
         });
        
         // Trigger the Bootstrap fade transition
         $info.addClass( 'in' );

         // Several things are happening here:
         //  1) This click event is still bubbling, listen to
         //     click now, and it will be caught before the popover
         //     ever appears.  Deferring it pushes the execution outside
         //     of the current call stack
         //  2) Clicks inside the popover are fine.  Use the $.has() function
         //     to see if any part of the target is or is inside the popover 
         //     element.  Only remove if that is not true.  Remember, has()
         //     returns a set of elements unlike is() which returns true/false.
         //  3) It makes sense to animate the box out.  Leave some time for that
         //     to happen before toasting the element from the DOM.
         _.defer( function() {
            $( document ).on( 'click.info', function( e ) {
               if ( $info.has( e.target ).length === 0 ) {
                  $info.removeClass( 'in' );
                  _.delay( function() { $info.remove(); }, 500 );
                  $( document ).off( 'click.info' );
               }
            });
         });
      },

      ...
   });


I did not delve into the tweaks to the styles I made for the demo. Those are really a matter of matching it to your current design. The demo also shows how multiple instances of MultiSearch can be used to add the CC and BCC lines without writing any extra code. Additionally, it also has some extra logic to properly mimic the input focus styles Bootstrap defines by adding some additional event handlers to the mix. Overall, MultiSearch does a lot of the heavy lifting and provides many points to add customized behavior. This demo covers a common use case the widget helps control and shows how to hook into those customizable points.