Wednesday, March 5, 2014

Customize the jQuery UI AutoComplete Drop Down Select Menu

When attempting to customize the jQuery UI Autocomplete widget's select box markup to match a Bootstrap drop down menu, I found that the hooks available are not part of the normal options hash. It seemed a little odd to provide this functionality in the form of a plugin extension and the documentation doesn't provide quite enough context for someone less familiar with the internals of the core widget library to actually implement the hook. The primary functions of interest are _renderItem and _renderMenu. Since they are prefixed with the "_", the widget factory will hide these methods from the plugin. So, in the documentation, you can see there is a method "close" which is called through the plugin on the target element:

   $( '#myac' ).autocomplete( 'close' )


however, you can't call _renderMenu in the same way:

   $( '#myac' ).autocomplete( '_renderMenu' )


That will throw an error. However, you can access anything in the widget instance object via $.data:

   var ac_inst = $( '#myac' ).data( 'ui-autocomplete' );


Now you can do fancy things with this object like change the behavior of _renderMenu:

   var ac_inst = $( '#myac' ).data( 'ui-autocomplete' ),
       old_renderMenu = ac_inst._renderMenu;
   
   ac_inst._renderMenu = function( ul, items ) {
       
       $( ul ).addClass( 'my-custom-ac-list-styles' )

       old_renderMenu.call( this, ul, items )
   }


That enables you to affect one Autocomplete widget at a time. What if you want to globally change all the Autocomplete widgets you create? In this case, you can just override the original widget using the factory method:

   $.widget( "ui.autocomplete", $.ui.autocomplete, {

      _renderItem: function( ul, item ) {
         return $( '<li>' )
            .attr( 'data-value', item.value )
            .append( $( '<a>' ).html( '<small>'+item.label+'</small>' ) )
            .appendTo( ul );
      },

      _renderMenu: function( ul, items ) {
         var that = this;
         $.each( items, function( index, item ) {
            that._renderItemData( ul, item );
         });
         $( ul )
            .attr( 'tabindex', -1 )
            .addClass( 'dropdown-menu' );

      },

   });


This works really well unless you want to adjust the behavior instance-by-instance. My preference is to mix the above two methods to extend the widget with actual options for the hooks and then defer to them, if defined, otherwise, use the default behavior:

   $.widget( "ui.autocomplete", $.ui.autocomplete, {

      options: {
         renderItem: null,
         renderMenu: null
      },

      _renderItem: function( ul, item ) {
         if ( $.isFunction( this.options.renderItem ) )
            return this.options.renderItem( ul, item );
         else
            return this._super( ul, item );
      },

      _renderMenu: function( ul, items ) {

         if ( $.isFunction( this.options.renderMenu ) ) {

            this.options.renderMenu( ul, items );

         }

         this._super( ul, items );
      },

   });


Now you can pass in functions to customize the markup when initializing the widget:

   $( '#myac' ).autocomplete({
      source: ['One', 'Two', 'Three'],

      renderMenu: function( ul, items ) {

         $( ul ).addClass( 'dropdown-menu' );
      },

      renderItem: function( ul, item ) {
         return $( '<li>' )
            .append( $( '<a>' ).html( '<i>'+item.label+'</i>' ) )
            .appendTo( ul );
      },

   });


And, if you want different types, you can always define different templates functions and swap them out as necessary:


   var acMenu_Base = function( ul, items ) {

         $( ul ).addClass( 'dropdown-menu' );
      },

      acItem_Italicize = function( ul, item ) {
         return $( '<li>' )
            .append( $( '<a>' ).html( '<i>'+item.label+'</i>' ) )
            .appendTo( ul );
      },

      acItem_Bold = function( ul, item ) {
         return $( '<li>' )
            .append( $( '<a>' ).html( '<b>'+item.label+'</b>' ) )
            .appendTo( ul );
      };

   $( '#myac_i' ).autocomplete({
      source: ['One', 'Two', 'Three'],

      renderMenu: acMenu_Base,
      renderItem: acItem_Italicize,

   });

   $( '#myac_b' ).autocomplete({
      source: ['Four', 'Five', 'Six'],

      renderMenu: acMenu_Base,
      renderItem: acItem_Bold,

   });



While maybe not the best example of affecting the individual items (a class on the UL could style them fine), it shows the basic idea. I setup this code in a jsFiddle so you can play with the definition and rendering options.