diff --git a/src-docs/src/views/combo_box/async.js b/src-docs/src/views/combo_box/async.js
new file mode 100644
index 000000000000..25cfa2ffc85b
--- /dev/null
+++ b/src-docs/src/views/combo_box/async.js
@@ -0,0 +1,118 @@
+import React, { Component } from 'react';
+
+import {
+ EuiComboBox,
+} from '../../../../src/components';
+
+const options = [{
+ value: 'titan',
+ label: 'Titan',
+ 'data-test-subj': 'titanOption',
+}, {
+ value: 'enceladus',
+ label: 'Enceladus',
+}, {
+ value: 'mimas',
+ label: 'Mimas',
+}, {
+ value: 'dione',
+ label: 'Dione',
+}, {
+ value: 'iapetus',
+ label: 'Iapetus',
+}, {
+ value: 'phoebe',
+ label: 'Phoebe',
+}, {
+ value: 'rhea',
+ label: 'Rhea',
+}, {
+ value: 'pandora',
+ label: 'Pandora is one of Saturn\'s moons, named for a Titaness of Greek mythology',
+}, {
+ value: 'tethys',
+ label: 'Tethys',
+}, {
+ value: 'hyperion',
+ label: 'Hyperion',
+}];
+
+export default class extends Component {
+ constructor(props) {
+ super(props);
+
+ this.options = options.slice();
+
+ this.state = {
+ isLoading: false,
+ isPopoverOpen: false,
+ selectedOptions: [this.options[2], this.options[4]],
+ };
+ }
+
+ onChange = (selectedOptions) => {
+ this.setState({
+ selectedOptions,
+ });
+ };
+
+ onSearchChange = (searchValue) => {
+ this.options = [];
+
+ this.setState({
+ isLoading: true,
+ });
+
+ clearTimeout(this.searchTimeout);
+
+ this.searchTimeout = setTimeout(() => {
+ this.options = options.filter(option => option.label.toLowerCase().includes(searchValue.toLowerCase()));
+ this.setState({
+ isLoading: false,
+ });
+ }, 1200);
+ }
+
+ onCreateOption =(searchValue, flattenedOptions) => {
+ const normalizedSearchValue = searchValue.trim().toLowerCase();
+
+ if (!normalizedSearchValue) {
+ return;
+ }
+
+ const newOption = {
+ value: searchValue,
+ label: searchValue,
+ };
+
+ // Create the option if it doesn't exist.
+ if (flattenedOptions.findIndex(option =>
+ option.value.trim().toLowerCase() === normalizedSearchValue
+ ) === -1) {
+ // Simulate creating this option on the server.
+ options.push(newOption);
+ this.options.push(newOption);
+ }
+
+ // Select the option.
+ this.setState(prevState => ({
+ selectedOptions: prevState.selectedOptions.concat(newOption),
+ }));
+ };
+
+ render() {
+ const { selectedOptions, isLoading } = this.state;
+
+ return (
+
+ );
+ }
+}
diff --git a/src-docs/src/views/combo_box/combo_box_example.js b/src-docs/src/views/combo_box/combo_box_example.js
index ac0f0b013d8a..3a143ac804a3 100644
--- a/src-docs/src/views/combo_box/combo_box_example.js
+++ b/src-docs/src/views/combo_box/combo_box_example.js
@@ -23,6 +23,10 @@ import DisallowCustomOptions from './disallow_custom_options';
const disallowCustomOptionsSource = require('!!raw-loader!./disallow_custom_options');
const disallowCustomOptionsHtml = renderToHtml(DisallowCustomOptions);
+import Async from './async';
+const asyncSource = require('!!raw-loader!./async');
+const asyncHtml = renderToHtml(Async);
+
export const ComboBoxExample = {
title: 'Combo Box',
sections: [{
@@ -75,5 +79,22 @@ export const ComboBoxExample = {
),
props: { EuiComboBox },
demo: ,
+ }, {
+ title: 'Async',
+ source: [{
+ type: GuideSectionTypes.JS,
+ code: asyncSource,
+ }, {
+ type: GuideSectionTypes.HTML,
+ code: asyncHtml,
+ }],
+ text: (
+
+ Use the onSearchChange code to handle searches asynchronously. Use the
+ isLoading prop to let the user know that something async is happening.
+
+ ),
+ props: { EuiComboBox },
+ demo: ,
}],
};
diff --git a/src/components/combo_box/combo_box.js b/src/components/combo_box/combo_box.js
index 06cfbf4df104..ab38d6953a58 100644
--- a/src/components/combo_box/combo_box.js
+++ b/src/components/combo_box/combo_box.js
@@ -24,6 +24,8 @@ import {
export class EuiComboBox extends Component {
static propTypes = {
className: PropTypes.string,
+ isLoading: PropTypes.bool,
+ async: PropTypes.bool,
options: PropTypes.array,
selectedOptions: PropTypes.array,
onChange: PropTypes.func.isRequired,
@@ -41,7 +43,7 @@ export class EuiComboBox extends Component {
const initialSearchValue = '';
const { options, selectedOptions } = props;
- const { matchingOptions, optionToGroupMap } = getMatchingOptions(options, selectedOptions, initialSearchValue);
+ const { matchingOptions, optionToGroupMap } = this.getMatchingOptions(options, selectedOptions, initialSearchValue);
this.state = {
searchValue: initialSearchValue,
@@ -59,6 +61,12 @@ export class EuiComboBox extends Component {
this.searchInput = undefined;
}
+ getMatchingOptions = (options, selectedOptions, searchValue) => {
+ // Assume the consumer has already filtered the options against the search value.
+ const isPreFiltered = this.props.async;
+ return getMatchingOptions(options, selectedOptions, searchValue, isPreFiltered);
+ };
+
openList = () => {
this.setState({
isListOpen: true,
@@ -137,6 +145,10 @@ export class EuiComboBox extends Component {
}
};
+ clearSearchValue = () => {
+ this.onSearchChange('');
+ };
+
removeLastOption = () => {
if (this.hasActiveOption()) {
return;
@@ -173,7 +185,7 @@ export class EuiComboBox extends Component {
// Add new custom pill if this is custom input, even if it partially matches an option..
if (!this.hasActiveOption() || this.doesSearchMatchOnlyOption()) {
this.props.onCreateOption(this.state.searchValue, flattenOptionGroups(this.props.options));
- this.setState({ searchValue: '' });
+ this.clearSearchValue();
}
};
@@ -186,7 +198,11 @@ export class EuiComboBox extends Component {
};
areAllOptionsSelected = () => {
- const { options, selectedOptions } = this.props;
+ const { options, selectedOptions, async } = this.props;
+ // Assume if this is async then there could be infinite options.
+ if (async) {
+ return false;
+ }
return flattenOptionGroups(options).length === selectedOptions.length;
};
@@ -247,7 +263,7 @@ export class EuiComboBox extends Component {
const { onChange, selectedOptions } = this.props;
onChange(selectedOptions.concat(addedOption));
this.clearActiveOption();
- this.setState({ searchValue: '' });
+ this.clearSearchValue();
this.searchInput.focus();
};
@@ -285,12 +301,11 @@ export class EuiComboBox extends Component {
});
};
- onSearchChange = (e) => {
+ onSearchChange = (searchValue) => {
if (this.props.onSearchChange) {
- this.props.onSearchChange();
+ this.props.onSearchChange(searchValue);
}
-
- this.setState({ searchValue: e.target.value })
+ this.setState({ searchValue });
};
comboBoxRef = node => {
@@ -335,7 +350,7 @@ export class EuiComboBox extends Component {
// Calculate and cache the options which match the searchValue, because we use this information
// in multiple places and it would be expensive to calculate repeatedly.
- const { matchingOptions, optionToGroupMap } = getMatchingOptions(options, selectedOptions, nextState.searchValue);
+ const { matchingOptions, optionToGroupMap } = this.getMatchingOptions(options, selectedOptions, nextState.searchValue);
this.matchingOptions = matchingOptions;
this.optionToGroupMap = optionToGroupMap;
@@ -351,11 +366,13 @@ export class EuiComboBox extends Component {
render() {
const {
className,
+ isLoading,
options,
selectedOptions,
- onChange, // eslint-disable-line no-unused-vars
onCreateOption,
+ onChange, // eslint-disable-line no-unused-vars
onSearchChange, // eslint-disable-line no-unused-vars
+ async, // eslint-disable-line no-unused-vars
...rest
} = this.props;
@@ -386,6 +403,7 @@ export class EuiComboBox extends Component {
/>
onChange(e.target.value)}
value={value}
ref={autoSizeInputRef}
inputRef={inputRef}
diff --git a/src/components/combo_box/combo_box_options_list/combo_box_options_list.js b/src/components/combo_box/combo_box_options_list/combo_box_options_list.js
index 46f001b5974f..be06bdba3392 100644
--- a/src/components/combo_box/combo_box_options_list/combo_box_options_list.js
+++ b/src/components/combo_box/combo_box_options_list/combo_box_options_list.js
@@ -2,14 +2,17 @@ import React from 'react';
import PropTypes from 'prop-types';
import { EuiCode } from '../../code';
+import { EuiFlexGroup, EuiFlexItem } from '../../flex';
import { EuiHighlight } from '../../highlight';
import { EuiPanel } from '../../panel';
import { EuiText } from '../../text';
+import { EuiLoadingSpinner } from '../../loading';
import { EuiComboBoxOption } from './combo_box_option';
import { EuiComboBoxTitle } from './combo_box_title';
export const EuiComboBoxOptionsList = ({
options,
+ isLoading,
selectedOptions,
onCreateOption,
searchValue,
@@ -23,30 +26,39 @@ export const EuiComboBoxOptionsList = ({
}) => {
let emptyStateContent;
- if (!options.length) {
- emptyStateContent = There aren’t any options available
;
- } else if (areAllOptionsSelected) {
- emptyStateContent = You’ve selected all available options
;
- } else if (matchingOptions.length === 0) {
- if (searchValue) {
- if (onCreateOption) {
- const selectedOptionForValue = getSelectedOptionForSearchValue(searchValue, selectedOptions);
- if (selectedOptionForValue) {
- // Disallow duplicate custom options.
- emptyStateContent = (
- {selectedOptionForValue.value} has already been added
- );
- } else {
- emptyStateContent = (
- Hit ENTER to add {searchValue} as a custom option
- );
- }
+ if (isLoading) {
+ emptyStateContent = (
+
+
+
+
+
+ Loading options
+
+
+ );
+ } else if (searchValue && matchingOptions.length === 0) {
+ if (onCreateOption) {
+ const selectedOptionForValue = getSelectedOptionForSearchValue(searchValue, selectedOptions);
+ if (selectedOptionForValue) {
+ // Disallow duplicate custom options.
+ emptyStateContent = (
+ {selectedOptionForValue.value} has already been added
+ );
} else {
emptyStateContent = (
- {searchValue} doesn’t match any options
+ Hit ENTER to add {searchValue} as a custom option
);
}
+ } else {
+ emptyStateContent = (
+ {searchValue} doesn’t match any options
+ );
}
+ } else if (!options.length) {
+ emptyStateContent = There aren’t any options available
;
+ } else if (areAllOptionsSelected) {
+ emptyStateContent = You’ve selected all available options
;
}
const emptyState = emptyStateContent ? (
@@ -107,6 +119,7 @@ export const EuiComboBoxOptionsList = ({
EuiComboBoxOptionsList.propTypes = {
options: PropTypes.array,
+ isLoading: PropTypes.bool,
selectedOptions: PropTypes.array,
onCreateOption: PropTypes.func,
searchValue: PropTypes.string,
diff --git a/src/components/combo_box/matching_options.js b/src/components/combo_box/matching_options.js
index 38e39cf32ed3..86bc46677dea 100644
--- a/src/components/combo_box/matching_options.js
+++ b/src/components/combo_box/matching_options.js
@@ -14,13 +14,19 @@ export const getSelectedOptionForSearchValue = (searchValue, selectedOptions) =>
return selectedOptions.find(option => option.label.toLowerCase() === normalizedSearchValue);
};
-const collectMatchingOption = (accumulator, option, selectedOptions, normalizedSearchValue) => {
+const collectMatchingOption = (accumulator, option, selectedOptions, normalizedSearchValue, isPreFiltered) => {
// Only show options which haven't yet been selected.
const selectedOption = getSelectedOptionForSearchValue(option.label, selectedOptions);
if (selectedOption) {
return false;
}
+ // If the options have already been prefiltered then we can skip filtering against the search value.
+ if (isPreFiltered) {
+ accumulator.push(option);
+ return;
+ }
+
if (!normalizedSearchValue) {
accumulator.push(option);
return;
@@ -32,7 +38,7 @@ const collectMatchingOption = (accumulator, option, selectedOptions, normalizedS
}
};
-export const getMatchingOptions = (options, selectedOptions, searchValue) => {
+export const getMatchingOptions = (options, selectedOptions, searchValue, isPreFiltered) => {
const normalizedSearchValue = searchValue.trim().toLowerCase();
const optionToGroupMap = new Map();
const matchingOptions = [];
@@ -41,12 +47,11 @@ export const getMatchingOptions = (options, selectedOptions, searchValue) => {
if (option.options) {
option.options.forEach(groupOption => {
optionToGroupMap.set(groupOption, option)
- collectMatchingOption(matchingOptions, groupOption, selectedOptions, normalizedSearchValue);
+ collectMatchingOption(matchingOptions, groupOption, selectedOptions, normalizedSearchValue, isPreFiltered);
})
} else {
- collectMatchingOption(matchingOptions, option, selectedOptions, normalizedSearchValue);
+ collectMatchingOption(matchingOptions, option, selectedOptions, normalizedSearchValue, isPreFiltered);
}
});
-
return { optionToGroupMap, matchingOptions };
};