Custom Select Menu with Visual Keyboard Input

Built using Google Closure Library

By day, I build web and mobile apps. Usually these contain dozens of separate components, which I constantly improve and reuse. Making excellent components requires a lot of iterations, therefore, I feel reusing and polishing existing ones as the best way to sustain their sensible evolution.

Projects changes, but your personal codebase is always at hand, being ready to help you building a next big thing faster.

Today I wish to unveil a custom select menu, a UI component that leverages the power of computer keyboard to find necessary item inside a list quicker. The component is built using Google Closure Library inheriting from goog.ui.Select class.

Preview of The Component

Animated preview of Custom Select Menu with Keyboard Input

As you can understand from an animated gif above, this select menu offers respectful solutions for 2 important problems:

  1. Scrolling the list is too slow to pick a needed item (especially with huge lists);
  2. Native <select> does not allow a very custom look and feel equal around browsers.

I work a lot on projects that manages data, where it is often relationally connected to some other data. Frequent the simplest way to update the connection between two single data items is using an interactive list.

For example, you have a list of all civil aviation airlines and wish to select Lufthansa. As there are around 1k airlines in the list, trying to scroll to a required airline will take time, however hitting L key on your keyboard is going to make it faster.
But what if there are a couple of dozen other airline names beginning with L letter? Then, probably, would be much faster to hit a combination of 2 - 3 keys on your keyboard at once to reach a desired airline (e.g. LU or LUF).

This kind of solution is going to save a lot of time if you have to manage lots of data, updating relations by hand.

Try it yourself Demo

This is a fully working demo of the component, underlining the potential of utilising the keyboard to help you reach a needed item extremely quick.
Try to select 'Lufthansa' airline from a list of civil aviation airlines below:

As you can see, there are multiple options to select 'Lufthansa':

How it is made

First of all, you can find component's source code under tn.CustomSelect namespace in our codebase repository on Github. Important to note, that it is a fully working component you can use safely in your JavaScript project if you also build with Closure Library.

Would be very helpful if you had source code opened aside before reading further. This would help to follow up code chunks presented underneath a bit easier.


As tn.CustomSelect inherits from goog.ui.Select, I think it will be respectful if I go only through properties and methods that extends constructor's prototype.

Let's begin with the constructor and its main properties:

tn.CustomSelect = function(opt_caption, opt_menu, opt_renderer,
    opt_domHelper, opt_menuRenderer) {

  /**
   * An array to store captured keys characters dispatched by the keyboard.
   * @type {Array<string>}
   * @private
   */
  this.keys_ = [];

  /**
   * An interval after which previously captured keys characters
   * shall be forgotten.
   * @type {number}
   * @private
   */
  this.keysForgetDelay_ = 2000;

  /**
   * Top offset from the beginning of the constructor's menu to a matched
   * item when a match is found and the menu is opened.
   * @type {number}
   * @private
   */
  this.itemFocusOffset_ = 0;

  // Calls a super class to inherit initial implementation from. This is
  // how custom components are build with Google Closure Library.
  tn.CustomSelect.base(this, 'constructor', opt_caption, opt_menu,
      opt_renderer, opt_domHelper, opt_menuRenderer);

}; goog.inherits(tn.CustomSelect, Select);


As you might guess after playing with the demo, the core logic of the component is in proper keyboard input events handling.

goog.ui.Select extends goog.ui.MenuButton, what makes its public methods available under our component's prototype as well. For example, handleKeyEventInternal method handles partial keyboard input behaviour. It shows/hides select menu when (Enter key) is pressed on the keyboard, or it provides respectful navigation within menu items using , .

By convention in the Closure Library, a method whose name ends in Internal (such as disposeInternal() or decorateInternal(), for example) is almost always a protected method that is designed to be overridden.
– Michael Bolin in "Closure: The Definitive Guide" book, page 186.

Relying on a quote above, we are safe to override handleKeyEventInternal method enriching it with additional keyboard input behavior:

tn.CustomSelect.prototype.handleKeyEventInternal = function(e) {
  if (this.getMenu().isVisible()) {
    switch (e.keyCode) {
      case KeyCodes.BACKSPACE:
        if (e.type === KeyHandler.EventType.KEY) {

          // When a Backspace key is pressed, previous item DOM match
          // gets cleared, last remembered key character gets forgotten
          // and a new DOM matching gets performed.
          this.clearMatchedItems_();
          this.keys_.pop();
          this.matchItem_();
        }

        // Default behaviour for the Backspace key gets prevented
        // to avoid navigating back to a previous page in some browsers.
        e.preventDefault();
        return false;
        break;

      case KeyCodes.ENTER:

        // When an Enter key is pressed, no overriding gets applied,
        // jumping down directly into the logic implemented by a
        // super class.
        break;

      case KeyCodes.UP:
      case KeyCodes.DOWN:
        if (e.type === KeyHandler.EventType.KEY) {

          // When Up or Down keys are pressed, previous item DOM match
          // gets cleared and captured keys characters gets forgotten,
          // because super class implements navigation behaviour within
          // select menu items for Up and Down keys.
          this.clearMatched();
        }

        break;

      default:
        if (KeyCodes.isTextModifyingKeyEvent(e)) {

          // When any other key is pressed and its event has text
          // modifying nature, charCode gets translated into a key
          // character, gets captured and a new DOM matching is performed
          // on menu item DOM nodes.
          var character = String.fromCharCode(e.charCode).toLowerCase();

          // Meanwhile, only English alphabet characters, numbers,
          // whitespace, hyphen and underscore characters gets captured.
          if (character && /[-_\sA-Z0-9]/gi.test(character)) {
            this.keys_.push(character);
            this.clearMatchedItems_();
            this.matchItem_();
          }

          e.preventDefault();
          return false;
        }
        break;
    }
  }

  // Calls super class to keep the logic it implements.
  return tn.CustomSelect.base(this, 'handleKeyEventInternal', e);
};


I think at this point would be valuable to know what clearMatchedItems_ and matchItem_ constructor's private method do.

clearMatchedItems_ clears previously matched item by setting back its initial text label:

tn.CustomSelect.prototype.clearMatchedItems_ = function() {
  var matches = this.getMenu().getElement().querySelectorAll(TagName.EM);

  array.forEach(matches, this.normaliseMenuItemLabel_, this);
};


tn.CustomSelect.prototype.normaliseMenuItemLabel_ = function(match) {
  var dom = this.getDomHelper();
  var parent = dom.getParentElement(match);

  dom.setTextContent(parent, dom.getTextContent(parent));
};


matchItem_ tries to find first menu item that has a label beginning with captured key characters string. If it finds one, item gets focused, part of the label gets wrapped into <em> HTML tag offering custom CSS styling (you have seen in the animated preview above):

tn.CustomSelect.prototype.matchItem_ = function() {
  var dom = this.getDomHelper();
  var keys = this.getKeysString();
  var keysLength = keys.length;

  array.some(array.range(this.getItemCount()), function(index) {
    var item = this.getItemAt(index);
    var itemEl = item.getElement();
    var caption = dom.getTextContent(itemEl);

    if (caption.toLowerCase().indexOf(keys) === 0) {

      // If item text label begins with a string of captured
      // keys characters, item gets focus.
      item.setHighlighted(true);

      // Matched part of the item label.
      var matched = caption.slice(0, keysLength);

      // Item label without matched part – just the rest.
      var remaining = caption.slice(keysLength);

      // Transforms item label DOM node by wrapping matched part into
      // `<em>` HTML tag.
      item.setContent(this.createMatchedItemDom_(matched, remaining));

      // Calculates menu container vertical scrollbar position
      // to assure found item gets visible (if items list is taller than
      // its menu container).
      var offset = style.getPosition(itemEl).y - this.itemFocusOffset_;
      this.getMenu().getElement().scrollTop = offset > 0 ? offset : 0;

      return true;
    }

    return false;

  }, this);

  // Forces to forget previously captured keys characters and to clear
  // menu matched item after customisable interval while key input
  // events are not being dispatched.
  utils.delay(this.clearMatched, this.keysForgetDelay_, this);
};

Worth to note that utils.delay at the bottom of matchItem_ method is a light dependency resolved by Closure Library and found inside tn.utils namespace in our codebase. It is used to assure previous matching is cleared when keyboard is not dispatching input events in a row within customisable interval defined under keysForgetDelay_ property in the constructor.

The purpose of this is to make sure you can start typing from the beginning after stopped hitting the keyboard for while. There is also setKeysForgetDelay public method which you can use to update the delay for your particular constructor instance.


When component enters the document, the only things what we shall implement on top of super class logic, is to always clear previously detected item match when the menu gets closed. A simplest way to achieve this is to attach a clearing logic into dispatched a goog.ui.Component.EventType.HIDE event:

tn.CustomSelect.prototype.enterDocument = function() {
  tn.CustomSelect.base(this, 'enterDocument');

  this.getHandler().
      listen(this, Component.EventType.HIDE, this.clearMatched);
};


That is it, more or less.
Last but not least, what I would love to remind, is how to create and initialise Google Closure Library UI component instances:

// Require everything needed to create an instance of the component:
goog.require('goog.dom.DomHelper');
goog.require('goog.events');
goog.require('goog.ui.Component.EventType');
goog.require('goog.ui.MenuItem');
goog.require('tn.CustomSelect');

var airlines = ['Air Baltic', 'Air Berlin', 'Austrian'];
var EventType = goog.ui.Component.EventType;

// Create a shared dom helper to use for the component
// and its children:
var domHelper = new goog.dom.DomHelper();

// Create an instance of the component:
var label = 'Select your favourite airline';
var select = new tn.CustomSelect(label, null, null, domHelper);

// Add items for your component's select menu:
airlines.forEach(function(airline, i) {
  select.addItem(new goog.ui.MenuItem(airline, i, domHelper));

});

// And render the component into document's body
// or inside a container (see `goog.ui.Component.render` method):
select.render();

// Here's how to watch when component's value gets changed:
goog.events.listen(select, EventType.CHANGE, function(e) {
  console.log(e.target.getValue());

});

Frequently Asked Questions

Here is a list of questions that appeared when I demonstrated the component to my colleagues and friends. Perhaps, you will find them useful too. Do not hesitate to ask any other question missing inside the list below.

1. Is this component source code compatible with Google Closure Compiler? Yes, it definitely is. Moreover, it is compiled with ADVANCED_OPTIMIZATIONS level for the demo above. You can include this component into your project and compile at ease.

2. Is it possible to reuse this component logic to build similar behaviour with jQuery, YUI, Dojo, MooTools or other alternative libraries? Yes, it is. The logic is as follows: add event listener to handle keypress events, capturing exposed keys characters (letters, numbers, etc.) and looping through menu items DOM nodes until find a label that begins with a string of previously captured keys characters. Then wrap matching part of a menu item label into <em> tag, so it could have a different CSS style applied.
I would be happy to see and make a direct reference from this article to your implementations built with alternative JavaScript libraries.

3. What about mobile use cases? I personally consider it would be much better if on mobile we still stay with native experience applied to HTML <select> tag. People are used to it and feel comfortable swiping down the list:

Select menu mobile use case

However, component works awesome on mobile anyway – you can open this page in your favourite internet browser on mobile device and try the demo.

4. How can I contribute if I want to? Just fork the repo on Github, add your improvement, report an issue or enrich the codebase with your methods and submit a pull request. I am sure you have lots of helpful functions, methods, constructors which are being constantly reused from project to project you build.

#component #uicomponent #closurelibrary #html #ui #gui #htmlselect #data