Element Framework

Combo Box

  • HTML
  • JS
  • CSS
<div class="wrapper">
  <ef-combo-box opened></ef-combo-box>
</div>
var comboBox = document.querySelector('ef-combo-box');

comboBox.data = [
  { label: 'EMEA', type: 'header' },
  { label: 'France', value: 'fr' },
  { label: 'Russian Federation', value: 'ru' },
  { label: 'Spain', value: 'es' },
  { label: 'United Kingdom', value: 'gb', selected: true },
  { label: 'APAC', type: 'header' },
  { label: 'China', value: 'ch' },
  { label: 'Australia', value: 'au' },
  { label: 'India', value: 'in' },
  { label: 'Thailand', value: 'th' },
  { label: 'AMERS', type: 'header' },
  { label: 'Canada', value: 'ca' },
  { label: 'United States', value: 'us' },
  { label: 'Brazil', value: 'br' },
  { label: 'Argentina', value: 'ar' }
];
.wrapper {
  padding: 5px;
  height: 300px;
}

ef-combo-box displays a text input and an associated pop-up element that helps users set a value.

Basic usage

The ef-combo-box uses the data property that follow ComboBoxData interface.

  • HTML
  • JS
  • CSS
<div class="wrapper">
  <ef-combo-box></ef-combo-box>
</div>
var comboBox = document.querySelector('ef-combo-box');
comboBox.data = [
  { label: 'EMEA', type: 'header' },
  { label: 'France', value: 'fr' },
  { label: 'Russian Federation', value: 'ru' },
  { label: 'Spain', value: 'es' },
  { label: 'United Kingdom', value: 'gb', selected: true },
  { label: 'APAC', type: 'header' },
  { label: 'China', value: 'ch' },
  { label: 'Australia', value: 'au' },
  { label: 'India', value: 'in' },
  { label: 'Thailand', value: 'th' },
  { label: 'AMERS', type: 'header' },
  { label: 'Canada', value: 'ca' },
  { label: 'United States', value: 'us' },
  { label: 'Brazil', value: 'br' },
  { label: 'Argentina', value: 'ar' }
];
.wrapper {
  padding: 5px;
  height: 300px;
}
<ef-combo-box></ef-combo-box>
const comboBox = document.querySelector('ef-combo-box');
comboBox.data = [
  { label: 'EMEA', type: 'header' },
  { label: 'France', value: 'fr' },
  { label: 'United Kingdom', value: 'gb', selected: true }
  // ...
];

Data property interface

The ef-combo-box uses the ComboBoxData interface for its data items which is described below.

Name Type Description
label string Item's label
value string Value of an item
selected boolean Selection state of the item
readonly boolean Sets the item to be readonly
disabled boolean Sets the item to be disabled

Value

When an item is selected, the item's value property will become Combo Box's new value.

Value can be set using the selected property or by programmatically setting the Combo Box value property.

comboBox.data = [
  // ...
  { label: 'United Kingdom', value: 'gb', selected: true }
  // ...
];
comboBox.value = 'gb';

Values

When Combo Box is in multiple mode it uses the values property to return multiple values.

Values can be set using the selected property or by programmatically setting the Combo Box values property.

comboBox.data = [
  // ...
  { label: 'United Kingdom', value: 'gb', selected: true },
  { label: 'Thailand', value: 'th', selected: true }
  // ...
];
comboBox.values = ['gb', 'th'];

Combo Box can only select data it already has.

Free Text mode

Set free-text to allow Combo Box to contain any arbitrary value. This mode is designed to cover a search input with suggestions scenario.

<ef-combo-box free-text></ef-combo-box>
  • HTML
  • JS
  • CSS
<div class="wrapper">
  <ef-combo-box free-text></ef-combo-box>
</div>
var comboBox = document.querySelector('ef-combo-box');
comboBox.data = [
  { label: 'EMEA', type: 'header' },
  { label: 'France', value: 'France' },
  { label: 'Russian Federation', value: 'Russian Federation' },
  { label: 'Spain', value: 'Spain' },
  { label: 'United Kingdom', value: 'United Kingdom' },
  { label: 'APAC', type: 'header' },
  { label: 'China', value: 'China' },
  { label: 'Australia', value: 'Australia' },
  { label: 'India', value: 'India' },
  { label: 'Thailand', value: 'Thailand' },
  { label: 'AMERS', type: 'header' },
  { label: 'Canada', value: 'Canada' },
  { label: 'United States', value: 'United States' },
  { label: 'Brazil', value: 'Brazil' },
  { label: 'Argentina', value: 'Argentina' }
];
.wrapper {
  padding: 5px;
  height: 300px;
}

Filtering

Default filtering is applied on the data label property. Filtering happens when the user modifies the input text.

The developer may wish to do their own filtering by implementing the filter property.

A typical example is to apply filter on multiple data properties (e.g. label and value as in the example below).

// Make a scoped re-usable filter for performance
const customFilter = (comboBox) => {
  let query = ''; // reference query string for validating queryRegExp cache state
  let queryRegExp; // cache RegExp

  // Get current RegExp, or renew if out of date
  // this is fetched on demand by filter/renderer
  // only created once per query
  const getRegularExpressionOfQuery = () => {
    if (comboBox.query !== query || !queryRegExp) {
      query = comboBox.query || '';
      queryRegExp = new RegExp(query.replace(/(\W)/g, '\\$1'), 'i');
    }
    return queryRegExp;
  };

  // return scoped custom filter
  return (item) => {
    const regex = getRegularExpressionOfQuery();
      // test on value or label
      const result = query === item.value || regex.test(item.label);
      regex.lastIndex = 0; // do not forget to reset last index
      return result;
    };
};

comboBox.filter = customFilter(comboBox);
  • HTML
  • JS
  • CSS
<div class="wrapper">
  <ef-combo-box placeholder="Type &quot;th&quot; or &quot;Thailand&quot;"></ef-combo-box>
</div>
var comboBox = document.querySelector('ef-combo-box');
comboBox.data = [
  { label: 'EMEA', type: 'header' },
  { label: 'France', value: 'fr' },
  { label: 'Russian Federation', value: 'ru' },
  { label: 'Spain', value: 'es' },
  { label: 'United Kingdom', value: 'gb' },
  { label: 'APAC', type: 'header' },
  { label: 'China', value: 'ch' },
  { label: 'Australia', value: 'au' },
  { label: 'India', value: 'in' },
  { label: 'Thailand', value: 'th' },
  { label: 'AMERS', type: 'header' },
  { label: 'Canada', value: 'ca' },
  { label: 'United States', value: 'us' },
  { label: 'Brazil', value: 'br' },
  { label: 'Argentina', value: 'ar' }
];
var customFilter = function(comboBox) {
  var query = '';
  var queryRegExp;
  var getRegularExpressionOfQuery = function() {
    if (comboBox.query !== query || !queryRegExp) {
      query = comboBox.query || '';
      queryRegExp = new RegExp(query.replace(/(\W)/g, '\\$1'), 'i');
    }
    return queryRegExp;
  };
  return function(item) {
    var regex = getRegularExpressionOfQuery();
    var result = query === item.value || regex.test(item.label);
    regex.lastIndex = 0; // do not forget to reset last index
    return result;
  };
};

comboBox.filter = customFilter(comboBox);
.wrapper {
  padding: 5px;
  height: 300px;
}

Regardless of filter configuration Combo Box always treats type: 'header' items as group headers, which persist as long as at least one item within the group is visible.

Asynchronous filtering

The component's built-in filter can only be used with pre-loaded data. However, you can still implement Asynchronous filtering by following these simple steps.

First, you need to remove the default filter:

comboBox.filter = null;

If the Combo Box value is set, you must ensure that the corresponding data item is always present.

if (comboBox.value) {
  comboBox.data = fetch(`/give-me-data?v=${comboBox.value}`);
}

To avoid excessive calls to the server you may want to set query-debounce-rate.

<ef-combo-box query-debounce-rate="200"></ef-combo-box>

Finally, listen for the query-changed event to make calls to the server and set the data property. Combo Box moves itself to the loading state.

combo.addEventListener('query-changed', async () => {
  comboBox.data = fetch(`/give-me-data?q=${comboBox.query}&v=${comboBox.value}`);
});

In the example below we mimic asynchronous filtering with setTimeout.

  • HTML
  • JS
  • CSS
<div class="wrapper">
  <ef-combo-box value="gb" query-debounce-rate="200"></ef-combo-box>
</div>
// A collection of data our search is based on
var data = [
  { label: 'France', value: 'fr' },
  { label: 'Russian Federation', value: 'ru' },
  { label: 'Spain', value: 'es' },
  { label: 'United Kingdom', value: 'gb' },
  { label: 'China', value: 'ch' },
  { label: 'Australia', value: 'au' },
  { label: 'India', value: 'in' },
  { label: 'Thailand', value: 'th' },
  { label: 'Canada', value: 'ca' },
  { label: 'United States', value: 'us' },
  { label: 'Brazil', value: 'br' },
  { label: 'Argentina', value: 'ar' }
];

var comboBox = document.querySelector('ef-combo-box');

// You **must** reset the default filter
comboBox.filter = null;

// A function to make request. In real life scenario it may wrap fetch
var request = function(query, value) {
  var regex = new RegExp(query.replace(/(\W)/g, '\\$1'), 'i');

  // Always keep a promise to let Combo Box know that the data is loading
  return new Promise(function(resolve) {
    var filterData = [];
    if (query || value) {
      for (var i = 0; i < data.length; i += 1) {
        var item = data[i];
        // Include element itself
        // Mark value hidden if it does not match search query
        if (value && item.value === value) {
          filterData.push(Object.assign({}, item, {
            selected: true,
            hidden: query ? !regex.test(item.label) : false
          }));
          regex.lastIndex = 0;
          continue;
        }

        if (query && regex.test(item.label)) {
          filterData.push(item);
          regex.lastIndex = 0;
        }
      }
    }
    setTimeout(function() {
      resolve(filterData);
    }, 500);
  });
}

// Populate self with the initial value
comboBox.data = request('', 'gb');

// Listen for query change event and make the request
comboBox.addEventListener('query-changed', function(event) {
  comboBox.data = request(comboBox.query, comboBox.value);
});
.wrapper {
  padding: 5px;
  height: 300px;
}

Custom renderer

Combo Box supports custom rendering by providing a renderer function to the renderer property. The renderer receives a data item, Collection Composer and previously mapped item elements (if any), and must return an HTMLElement.

The preferred approach is to extend the DefaultRender that comes with Combo Box. The default renderer uses Item elements, and supports highlighted, selected, disabled, hidden and readonly states.

import { DefaultRenderer } from '../lib/ef-combo-box';

// Create a re-useable renderer that shows Flags next to the country
class FlagRender extends DefaultRenderer {
  constructor (comboBox) {
    // Keep the reference to the default renderer
    const defaultRenderer = super(comboBox);
    // store reference to flag for easy access.
    // Use WeakMap to not care about memory leaks
    const flagMap = new WeakMap();

    // Return the closure
    return (item, composer, element) => {
      element = defaultRenderer(item, composer, element);
      const type = composer.getItemPropertyValue(item, 'type');
      let flagElement = flagMap.get(element);
      if (!flagElement && (!type || type === 'text')) {
        // Text items
        flagElement = document.createElement('ef-flag');
        flagElement.slot = 'left'; // use ef-item slotted content
        element.appendChild(flagElement);
        flagMap.set(element, flagElement);
      }
      else if (flagElement && type && type !== 'text') {
        // Header items, which should not have a flag
        // Make sure that flag element is removed
        flagElement.parentNode.removeChild(flagElement);
        flagElement.remove(element, flagElement);
        flagElement = null;
      }

      // Make sure that you can re-use the same element with new data item
      if (flagElement) {
        flagElement.flag = composer.getItemPropertyValue(item, 'value');
      }

      return element;
    };
  }
}
comboBox.renderer = new FlagRender(comboBox);

As an alternative you can provide your own renderer. If you go that route, you must ensure that, at a minimum, the highlighted, selected and hidden states are covered.

comboBox.renderer = (item, composer, element) => {
  // Make sure to re-use the same element for increased performance
  if (!element) {
    element = document.createElement('div');
    element.style.setProperty('margin', '5px 10px');
    element.style.setProperty('padding', '5px 0');
  }

  // All item properties are read using the Collection Composer
  const type = composer.getItemPropertyValue(item, 'type');
  const label = composer.getItemPropertyValue(item, 'label');
  const selected = composer.getItemPropertyValue(item, 'selected') === true;
  const highlighted = composer.getItemPropertyValue(item, 'highlighted') === true;
  const hidden = composer.getItemPropertyValue(item, 'hidden') === true;

  // Style the element accordingly
  element.style.setProperty('display', hidden ? 'none': 'block');
  element.textContent = label;

  let colour = 'grey';
  if (type === 'header') {
    colour = 'red';
  }
  else if (highlighted) {
    colour = 'green';
  }
  else if (selected) {
    colour = 'blue';
  }

  element.style.setProperty('color', colour);

  return element;
};
  • HTML
  • JS
  • CSS
<div class="wrapper">
  <ef-combo-box opened></ef-combo-box>
</div>
var comboBox = document.querySelector('ef-combo-box');

comboBox.data = [
  { label: 'EMEA', type: 'header' },
  { label: 'France', value: 'fr' },
  { label: 'Russian Federation', value: 'ru' },
  { label: 'Spain', value: 'es' },
  { label: 'United Kingdom', value: 'gb', selected: true },
  { label: 'APAC', type: 'header' },
  { label: 'China', value: 'ch' },
  { label: 'Australia', value: 'au' },
  { label: 'India', value: 'in' },
  { label: 'Thailand', value: 'th' },
  { label: 'AMERS', type: 'header' },
  { label: 'Canada', value: 'ca' },
  { label: 'United States', value: 'us' },
  { label: 'Brazil', value: 'br' },
  { label: 'Argentina', value: 'ar' }
];

// Use ES5 syntax here for compatibility
// If possible, use ESM import and classes instead:
// import { DefaultRenderer } from '../lib/ef-combo-box';
// class FlagRender extends DefaultRenderer { /* ... */ }
var createFlagRender = function(context) {
  // Keep the reference to the default renderer
  var defaultRenderer = context.renderer;

  // store reference to flag for easy access.
  // Use WeakMap to not care about memory leaks
  var flagMap = new WeakMap();

  // Return the closure
  return function(item, composer, element) {
    element = defaultRenderer(item, composer, element);
    var type = composer.getItemPropertyValue(item, 'type');
    var flagElement = flagMap.get(element);
    if (!flagElement && (!type || type === 'text')) {
      // Text items
      flagElement = document.createElement('ef-flag');
      flagElement.slot = 'left';
      element.appendChild(flagElement);
      flagMap.set(element, flagElement);
    } else if (flagElement && type && type !== 'text') {
      // Header items, which should not have a flag
      // Make sure that flag element is removed
      flagElement.parentNode.removeChild(flagElement);
      flagMap.remove(element, flagElement);
      flagElement = null;
    }

    // Make sure that you can re-use the same element with new data item
    if (flagElement) {
      flagElement.flag = composer.getItemPropertyValue(item, 'value');
    }

    return element;
  };
};

var setRenderer = function() {
  comboBox.renderer = createFlagRender(comboBox);
};

if (customElements.get('ef-combo-box')) {
  setRenderer();
} else {
  customElements.whenDefined('ef-combo-box').then(setRenderer);
}
.wrapper {
  padding: 5px;
  height: 300px;
}

Customize popup panel size

By default the popup width is equivalent to the input box. However, it can be overridden using CSS.

CSS Variable Name Description
--list-max-width Max width of popup list
--list-max-height Max height of popup list

API Reference

Attributes

boolean
multiple
Multiple selection mode
boolean
opened
Track opened state of popup
string
placeholder
Placeholder for input field
boolean
clears
Show clears button
boolean
free-text
Allow to enter any value
boolean
error
Set state to error
boolean
warning
Set state to warning
number
query-debounce-rate
Control query rate, defaults to 0
string
value
Returns the first selected item value. Use `values` when multiple selection mode is enabled.
boolean
readonly
Set readonly state
boolean
disabled
Set disabled state
string
name
Set name of the element

Properties

ComboBoxFilter<T> | null
filter
Custom filter for static data Set this to null when data is filtered externally, eg XHR
"defaultFilter<T>(this)"
ComboBoxRenderer
renderer
Renderer used to render list item elements
"new ComboBoxRenderer(this)"
boolean
multiple
Multiple selection mode
boolean
opened
Track opened state of popup
false
string
placeholder
Placeholder for input field
""
boolean
clears
Show clears button
false
boolean
freeText
Allow to enter any value
boolean
error
Set state to error
false
boolean
warning
Set state to warning
false
number
queryDebounceRate
Control query rate, defaults to 0
ComboBoxData<T>
data
Data array to be displayed
string
value
Returns the first selected item value. Use `values` when multiple selection mode is enabled.
string[]
values
Returns a values collection of the currently selected item values
string | null
query
Query string applied to combo-box Set via internal text-field input
string
label
Label of selected value
boolean
readonly
Set readonly state
false
boolean
disabled
Set disabled state
false
string
name
Set name of the element
"''"

Events

value-changed
Dispatched when value changes
query-changed
Dispatched when query changes
opened-changed
Dispatched when opened state changes