Skip to content
24/06/2010 / Danresa Consultoria de Informática

A jQuery UI Combobox: Under the hood

by Jörn Zaefferer

The jQuery UI 1.8 release brings along the new autocomplete widget. An autocomplete adds a list of suggestions to an input field, displayed and filtered while the user is typing. This could be attached to a search field, suggesting either search terms or just matching results for faster navigation. But what if there is a fixed list of options, usually implemented as a standard HTML select element, where the ability to filter would help users find the right value way faster?

That’s a “combobox.” A combobox works like a select, but also has an input field to filter the options by typing. jQuery UI 1.8 actually provides a sample implementation of a combobox as a demo. In this article, we’ll look under the hood of the combobox demo, to explore both the combobox widget and the autocomplete widget that it uses.

Let’s starts with the initial markup:

HTML:

  1. <label>Your preferred programming language: </label>
  2. <select>
  3.   <option value=”a”>asp</option>
  4.   <option value=”c”>c</option>
  5.   <option value=”cpp”>c++</option>
  6.   <option value=”cf”>coldfusion</option>
  7.   <option value=”g”>groovy</option>
  8.   <option value=”h”>haskell</option>
  9.   <option value=”j”>java</option>
  10.   <option value=”js”>javascript</option>
  11.   <option value=”p1″>perl</option>
  12.   <option value=”p2″>php</option>
  13.   <option value=”p3″>python</option>
  14.   <option value=”r”>ruby</option>
  15.   <option value=”s”>scala</option>
  16. </select>

Nothing special there, just a label and a select element with a few options.

The code to apply the combobox widget to the select is quite simple, too:

JavaScript:

  1. $(“select”).combobox();

Let’s look at the code for this combobox widget. First, the full code, to give you an overview. We’ll dig into the details step-by-step afterwards.

JavaScript:

  1. $.widget(“ui.combobox”, {
  2.   _create: function() {
  3.     var self = this;
  4.     var select = this.element.hide();
  5.     var input = $(“<input />”)
  6.       .insertAfter(select)
  7.       .autocomplete({
  8.         source: function(request, response) {
  9.           var matcher = new RegExp(request.term, “i”);
  10.           response(select.children(“option”).map(function() {
  11.             var text = $(this).text();
  12.             if (this.value && (!request.term || matcher.test(text)))
  13.               return {
  14.                 id: this.value,
  15.                 label: text.replace(new RegExp(“(?![^&;]+;)(?!<[^<>]*)(” + $.ui.autocomplete.escapeRegex(request.term) + “)(?![^<>]*>)(?![^&;]+;)”, “gi”), “<strong>$1</strong>”),
  16.                 value: text
  17.               };
  18.           }));
  19.         },
  20.         delay: 0,
  21.         change: function(event, ui) {
  22.           if (!ui.item) {
  23.             // remove invalid value, as it didn’t match anything
  24.             $(this).val(“”);
  25.             return false;
  26.           }
  27.           select.val(ui.item.id);
  28.           self._trigger(“selected”, event, {
  29.             item: select.find(“[value='” + ui.item.id + “‘]”)
  30.           });
  31.          
  32.         },
  33.         minLength: 0
  34.       })
  35.       .addClass(“ui-widget ui-widget-content ui-corner-left”);
  36.     $(“<button> </button>”)
  37.     .attr(“tabIndex”, -1)
  38.     .attr(“title”, “Show All Items”)
  39.     .insertAfter(input)
  40.     .button({
  41.       icons: {
  42.         primary: “ui-icon-triangle-1-s”
  43.       },
  44.       text: false
  45.     }).removeClass(“ui-corner-all”)
  46.     .addClass(“ui-corner-right ui-button-icon”)
  47.     .click(function() {
  48.       // close if already visible
  49.       if (input.autocomplete(“widget”).is(“:visible”)) {
  50.         input.autocomplete(“close”);
  51.         return;
  52.       }
  53.       // pass empty string as value to search for, displaying all results
  54.       input.autocomplete(“search”, “”);
  55.       input.focus();
  56.     });
  57.   }
  58. });

Let’s break this down, piece by piece:

JavaScript:

  1. $.widget(“ui.combobox”, {
  2.   _create: function() {
  3.     // all the code
  4.   }
  5. });

This defines a new widget, in the ui namespace (don’t use this for your own widgets, it’s reserved for jQuery UI widgets) and adds the only method, _create. This is the constructor method for jQuery UI widgets, and will be called only once. In versions prior to 1.8 it was called _init. The _init method still exists, but it is called each time you call .combobox() (with or without options). Keep in mind that our widget implementation is not complete, as it lacks the destroy method. It’s just a demo.

Coming up next is the creation of an input element and applying the autocomplete to it, with data provided by the select element.

JavaScript:

  1. var self = this;
  2. var select = this.element.hide();
  3. var input = $(“<input />”)
  4.   .autocomplete({
  5.     source: function(request, response) {
  6.       // implements retrieving and filtering data from the select
  7.     },
  8.     delay: 0,
  9.     change: function(event, ui) {
  10.       // implements updating the select with the selection
  11.     },
  12.     minLength: 0
  13.   })
  14.   .addClass(“ui-widget ui-widget-content ui-corner-left”);

It starts with a few variable declarations: var self = this will be used inside callbacks below, where this will refer to something else. The var select references the select element on which the combobox gets applied. To replace the select with the text input, the select is hidden.

Next, an input element is created from scratch, inserted after the select element into the DOM, and transformed into an autocomplete widget. All three autocomplete options are customized:

  • source provides the filtered data to display
  • delay specifies the amount of time to wait for displaying data between each key press, here set to zero as the data is local
  • minLength is set to 0, too, so that a cursor-down or -up key press will display the autocomplete menu, even when nothing was entered.

Let’s break down the source implementation:

JavaScript:

  1. source: function(request, response) {
  2.   var matcher = new RegExp(request.term, “i”);
  3.   response(select.children(“option”).map(function() {
  4.     var text = $(this).text();
  5.     if (this.value && (!request.term || matcher.test(text)))
  6.       return {
  7.         id: this.value,
  8.         label: text.replace(new RegExp(“(?![^&;]+;)(?!<[^<>]*)(” + $.ui.autocomplete.escapeRegex(request.term) + “)(?![^<>]*>)(?![^&;]+;)”, “gi”), “<strong>$1</strong>”),
  9.         value: text
  10.       };
  11.   }));
  12. },

There is a bit of matching and mapping involved here: At first, a regular expression object is defined, based on the entered term. That gets reused in the function below. The response argument, a callback, gets called, to provide the data to display. The argument passed is the result of the call to select.find("option".map(callback). That finds all option elements within our original select, then maps each option to a different object, implemented in another callback passed to the map method.

This callback will return undefined, thereby removing an item, when a search term is present and the text of the option doesn’t match the entered value. Otherwise (no term, or it matches), it’ll return an object with three properties:

  • id: the value attribute of the option, will later be used to update the select element with a new selection
  • label: based on the text of the option, with the matched term highlighted with some regexing (another example of a write-only regular expression)
  • value: the unmodified text of the option, to be inserted into the text input field

The label and value properties are expected by the autocomplete widget, the id property has an arbitrary name, used here only by the combobox widget.

Before, I mentioned that the combobox wideget customizes all three autocomplete options, but there were actually four options specified. The fourth property here, change, is actually an event. This is the implementation:

JavaScript:

  1. change: function(event, ui) {
  2.   if (!ui.item) {
  3.     // remove invalid value, as it didn’t match anything
  4.     $(this).val(“”);
  5.     return false;
  6.   }
  7.   select.val(ui.item.id);
  8.   self._trigger(“selected”, event, {
  9.     item: select.find(“[value='” + ui.item.id + “‘]”)
  10.   });
  11.  
  12. },

The ui.item argument refers to the data we provided in the source option. In case the user entered a value not provided, ui.item is null, which is used here to drop that input, by setting the value of the input element to an empty string. If an item is present, the select is updated to match that item, using ui.item.id, which refers to the value attribute of the associated option element. And, for further customization for someone using the combobox widget, a selected event is triggered.

The next code block creates the button that opens the full list of options:

JavaScript:

  1. $(“<button>&nbsp;</button>”)
  2. .attr(“tabIndex”, -1)
  3. .attr(“title”, “Show All Items”)
  4. .insertAfter(input)
  5. .button({
  6.   icons: {
  7.     primary: “ui-icon-triangle-1-s”
  8.   },
  9.   text: false
  10. }).removeClass(“ui-corner-all”)
  11. .addClass(“ui-corner-right ui-button-icon”)
  12. .click(function() {
  13.   // close if already visible
  14.   if (input.autocomplete(“widget”).is(“:visible”)) {
  15.     input.autocomplete(“close”);
  16.     return;
  17.   }
  18.   // pass empty string as value to search for, displaying all results
  19.   input.autocomplete(“search”, “”);
  20.   input.focus();
  21. });

Another element is created on-the-fly. It gets tabIndex="-1" to take it out of the tab order, as it’s mostly useful for mouse interactions. Keyboard interaction is already covered by the input element. It gets a title attribute to provide a tooltip and is inserted after the input element into the DOM. A call to .button() with some options together with a bit of class-mangling transforms the button into a Button widget that displays a down-arrow icon with rounded corners on the right (the input has rounded-corners on the left).

Finally a click event is bound to the button: If the autocomplete menu is already visible, it gets closed, otherwise the autocomplete’s search method is called with an empty string as the argument, to search for all elements, independent of the current value within the input. As the input handles keyboard input, it gets focused. Having focus on the button would be useless or would require duplicate keyboard interaction that the input already supports.

And that’s it! We can see that the autocomplete widget is flexible enough to allow all this with option customization, events, and calling a few methods. We don’t have to “subclass” autocomplete (creating a new widget with the autocomplete as the parent prototype instead of $.widget). Instead, we can make the combobox independent of any internal or private autocomplete methods. Check out the combobox demo on the jQuery UI site.

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: