NUI-Widgets documentation has moved to Storybook hosted on Github Pages.

AutoCompleteFilter

There is a breaking change to this component in the next version of NUI Widgets.
  • The readOnly prop will behave differently in a future version of NUI Widgets. The Button will no longer appear in readOnly AutoCompleteFilters.
import AutoCompleteFilter from '@concur/nui-widgets/lib/AutoComplete/AutoCompleteFilter';

AutoComplete components allow users to select items from a defined list. The input field acts as a search box and will narrow the list of items presented based on matches against the entered text.

AutoCompleteFilter includes a secondary dropdown that allows results to be filtered based on some criteria. For example, the data array could contain a list of all employee names while the filterData array enables filtering of employees by department.

The HTML autocomplete attribute for the input element is enabled by default meaning browsers will try and provide suggestions for input fields. SAP Concur components that provide dropdowns or popovers to assist with populating an input field attempt to disable the browser's suggestions. Please open an issue if you are seeing browser suggestion behavior for these components. See the support page for more information.

Examples

Simple Filter

A basic list of items in the filter dropdown to filter data in the autocomplete dropdown.

const data = [
    {fname: 'Adam', lname: 'Fedor', empid: '0601', email: 'adam.fedor@concur.com', location: 'Bellevue', dept: 'PM'},
    {fname: 'Maria', lname: 'Gandarillas', empid: '0701', email: 'maria.gandarillas@concur.com', location: 'Bellevue', dept: 'PM'},
    {fname: 'Campbell', lname: 'Gunn', empid: '0702', email: 'campbell.gunn@concur.com', location: 'Bellevue', dept: 'PM'},
    {fname: 'Jeffrey', lname: 'Johnson', empid: '1001', email: 'jeffrey.johnson@concur.com', location: 'Bellevue', dept: 'DEV'},
    {fname: 'Peter', lname: 'Kim', empid: '1101', email: 'peter.kim@concur.com', location: 'Vienna', dept: 'QA'},
    {fname: 'Matthew', lname: 'Osborn', empid: '1501', email: 'matthew.osborn@concur.com', location: 'Fargo (remote)', dept: 'DEV'},
    {fname: 'Greg', lname: 'Smith', empid: '1901', email: 'greg.a.smith@concur.com', location: 'Minneapolis (remote)', dept: 'DEV'},
    {fname: 'Brad', lname: 'Ullman', empid: '2101', email: 'brad.ullman@concur.com', location: 'Bellevue', dept: 'DEV'},
    {fname: 'Darin', lname: 'Warling', empid: '2301', email: 'darin.warling@concur.com', location: 'Minneapolis (remote)', dept: 'DEV'}
];

const mruData = data.slice(2, 5);

const filterData = [
    {text: 'All', id: '0', filterCode: '', active: true},
    {text: 'Development', id: '1', filterCode: 'DEV'},
    {text: 'Quality Assurance', id: '2', filterCode: 'QA'},
    {text: 'Project Management', id: '3', filterCode: 'PM'}
];

class AutoCompleteFilterExample extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            // If no default filter has been specified, automatically pick the first one in the list:
            activeFilterItemId: ( this.props.activeFilterItemId ? this.props.activeFilterItemId : filterData[0].id ),

            data: data,
            mruData: mruData,
            filterData: (this.props.loadFilterDataAfterInitialRender ? [] : this.props.filterData || filterData),
            searchText: (this.props.value ? this.props.value : ''),
            filter: ''
        };
    }

    componentDidMount() {
        if (this.props.loadFilterDataAfterInitialRender) {
            // this is an example to show `filterData` being applied after the initial render
            // so we are disabling the lint rule that says not to setState in componentDidMount
            this.setState({ // eslint-disable-line
                filterData: this.props.filterData || filterData
            });
        }
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.value !== this.state.searchText) {
            this.setState({
                searchText: nextProps.value
            });
        }
    }

    applyFilter = (itemData) => {
        return this.state.filter ? this.state.filter === itemData.dept : true;
    }

    filterByText = (text, itemData) => {
        if (!text) {
            return true;
        } else if (itemData.fname.toLowerCase().indexOf(text.toLowerCase()) >= 0 ||
            itemData.lname.toLowerCase().indexOf(text.toLowerCase()) >= 0 ||
            itemData.empid.toLowerCase().indexOf(text.toLowerCase()) >= 0 ||
            itemData.email.toLowerCase().indexOf(text.toLowerCase()) >= 0 ||
            itemData.location.toLowerCase().indexOf(text.toLowerCase()) >= 0) {
            return true;
        }

        return false;
    }

    handleChange = (text) => {
        this.setState({
            searchText: text,
            data: data.filter(rec => {
                return this.filterByText(text, rec) && this.applyFilter(rec);
            }),
            mruData: mruData.filter(rec => {
                return this.filterByText(text, rec) && this.applyFilter(rec);
            })
        });
    }

    handleFilterSelect = (item) => {
        if (item) {
            const rec = getRecordById(this.state.filterData, item.props.id);
            if (rec) {
                this.setState({
                    activeFilterItemId: item.props.id,
                    filter: rec.filterCode
                }, () => {
                    this.handleChange(this.state.searchText);
                });
            }
        }
    }

    handleSelect = (itemData) => {
        this.setState({
            searchText: this._renderInputText(itemData),
            data: data.filter(rec => {
                return rec.empid === itemData.empid;
            }),
            mruData: mruData.filter(rec => {
                return rec.empid === itemData.empid;
            })
        });
    }

    _renderInputText = (itemData) => {
        return itemData.lname + ', ' + itemData.fname;
    }

    render() {

        const {
            loadFilterDataAfterInitialRender,
            ...otherProps
        } = this.props;

        return (
            <AutoCompleteFilter
                {...otherProps}
                activeFilterItemId={this.state.activeFilterItemId}
                data={this.state.data}
                dataRendering={{
                    inputRenderer: this._renderInputText,
                    listItemRenderer: (itemData) => {
                        return <Employee {...itemData} />;
                    }
                }}
                dropdownTitle='filter'
                filterData={this.state.filterData}
                itemKey={'empid'}
                mruData={this.state.mruData}
                mruTitle='Most Recently Used'
                onChange={(e) => this.handleChange(e.target.value)}
                onFilterSelect={this.handleFilterSelect}
                onSelect={this.handleSelect}
                value={this.state.searchText} />
        );
    }
}

Searchable Filter

When the list of items in the filter gets larger, you can enable a search field in the filter dropdown. Use the showFilterSearch property to enable searching.

class AutoCompleteLocationExample extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            activeFilterItemId: ( this.props.activeFilterItemId ? this.props.activeFilterItemId : filterData[0].id ),
            data: data,
            filterData: filterData,
            searchText: '',
            filter: ''
        };
    }

    filterByText = (text, itemData) => {
        return !text ? true : itemData.text.toLowerCase().indexOf(text.toLowerCase()) >= 0;
    }

    filterByCountry = (itemData) => {
        return this.state.filter ? this.state.filter === itemData.countryCode : true;
    }

    handleChange = (text) => {
        this.setState({
            searchText: text,
            data: data.filter(rec => {
                return this.filterByText(text, rec) && this.filterByCountry(rec);
            })
        });
    }

    handleFilterSelect = (item) => {
        if (item) {
            const rec = getRecordById(this.state.filterData, item.props.id);
            if (rec) {
                this.setState({
                    activeFilterItemId: item.props.id,
                    filter: rec.countryCode
                }, () => {
                    this.handleChange(this.state.searchText);
                });
            }
        }
    }

    handleSelect = (loc) => {
        this.setState({
            searchText: loc.text,
            data: data.filter(rec => {
                return rec.text.toLowerCase().indexOf(loc.text.toLowerCase()) >= 0;
            })
        });
    }

    render() {
        return (
            <AutoCompleteFilter
                active={this.props.active}
                activeFilterItemId={this.state.activeFilterItemId}
                data={this.state.data}
                dropdownIcon='icon-globe'
                dropdownTitle='filter'
                filterData={this.state.filterData}
                mruData={this.state.mruData}
                mruTitle='Most Recently Used'
                noResultsMsg={this.props.noResultsMsg}
                onChange={(e) => this.handleChange(e.target.value)}
                onFilterSelect={this.handleFilterSelect}
                onSelect={this.handleSelect}
                showFilterSearch
                size={this.props.size}
                value={this.state.searchText} />
        );
    }
}

Custom Rendering

To render custom content for list items and input text, use the dataRendering property. See AutoComplete for an example.

Headers, Dividers and Disabled Items

See AutoComplete for an example of how to use headers, dividers or disabled items in the dropdown list.

Loading Message using Spinner

The loadingMsg property can be used to add a Spinner to the dropdown loading animation.

const loadingSpinner = (
    <React.Fragment>
        <Spinner
            message='Loading...'
            size='lg'
            type='inline'
            visible />
        <span> Loading...</span>
    </React.Fragment>
);

const AutoCompleteFilterEx = () => (
    <AutoCompleteFilter
        isLoading
        loadingMsg={loadingSpinner} />
);

Infinite Scrolling

To use infinite scrolling, the consumer must provide and call the required data using the infinite object. The onLoadMore callback is triggered near the bottom of the dropdown to allow for more data to be fetched. The new data should be appended to the previous data property. To stop infinite scrolling or stop fetching data, change hasMore to false.

class HIGAutoCompleteFilterEx05 extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            data: data.slice(1, 50),
            filterData: filterData,
            hasMore: true,
            maxData: 50,
            searchText: (this.props.value ? this.props.value : '')
        };
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.value !== this.state.searchText) {
            this.setState({
                searchText: nextProps.value
            });
        }
    }

    filterByCountry = (itemData) => {
        return this.state.filter ? this.state.filter === itemData.countryCode : true;
    }

    filterByText = (text, itemData) => {
        if (!text) {
            return true;
        } else if (itemData.text.toLowerCase().indexOf(text.toLowerCase()) >= 0) {
            return true;
        }

        return false;
    }

    getMoreData = () => {
        if (this.state.maxData > data.length) {
            this.setState({
                hasMore: false
            });
        }

        setTimeout(() => {
            this.setState({
                data: data.slice(0, this.state.maxData + 50),
                maxData: this.state.maxData + 50
            });
        }, 750);
    }

    handleChange = (text) => {
        this.setState({
            searchText: text,
            data: this.state.data.filter(rec => {
                return this.filterByText(text, rec) && this.filterByCountry(rec);
            })
        });
    }

    handleFilterSelect = (item) => {
        if (item) {
            const rec = getRecordById(this.state.filterData, item.props.id);
            if (rec) {
                this.setState({
                    activeFilterItemId: item.props.id,
                    filter: rec.countryCode
                }, () => {
                    this.handleChange(this.state.searchText);
                });
            }
        }
    }

    handleSelect = (itemData) => {
        this.setState({
            searchText: itemData.text,
            data: this.state.data.filter(rec => {
                return rec.id === itemData.id;
            })
        });
    }

    render() {

        const loadingSpinner = (
            <>
                <Spinner
                    message='Loading...'
                    size='lg'
                    type='inline'
                    visible />
                <span> Loading...</span>
            </>
        );
        return (
            <div className='row'>
                <div className='col-xs-8 col-sm-6 col-md-4'>
                    <AutoCompleteFilter
                        {...this.props}
                        data={this.state.data}
                        dropdownIcon='globe'
                        dropdownTitle='filter'
                        filterData={this.state.filterData}
                        infinite={{
                            hasMore: this.state.hasMore,
                            onLoadMore: this.getMoreData,
                            threshold: 100
                        }}
                        loadingMsg={loadingSpinner}
                        onChange={(e) => this.handleChange(e.target.value)}
                        onFilterSelect={this.handleFilterSelect}
                        onSelect={this.handleSelect}
                        value={this.state.searchText} />
                </div>
            </div>
        );
    }
}

Disabled State

Use the disabled property to disable the AutocompleteFilter component. A disabled element is unusable and un-clickable.

const data = [
    {fname: 'Adam', lname: 'Fedor', empid: '0601', email: 'adam.fedor@concur.com', location: 'Bellevue', dept: 'PM'},
    {fname: 'Maria', lname: 'Gandarillas', empid: '0701', email: 'maria.gandarillas@concur.com', location: 'Bellevue', dept: 'PM'},
    {fname: 'Campbell', lname: 'Gunn', empid: '0702', email: 'campbell.gunn@concur.com', location: 'Bellevue', dept: 'PM'},
    {fname: 'Jeffrey', lname: 'Johnson', empid: '1001', email: 'jeffrey.johnson@concur.com', location: 'Bellevue', dept: 'DEV'},
    {fname: 'Peter', lname: 'Kim', empid: '1101', email: 'peter.kim@concur.com', location: 'Vienna', dept: 'QA'},
    {fname: 'Matthew', lname: 'Osborn', empid: '1501', email: 'matthew.osborn@concur.com', location: 'Fargo (remote)', dept: 'DEV'},
    {fname: 'Greg', lname: 'Smith', empid: '1901', email: 'greg.a.smith@concur.com', location: 'Minneapolis (remote)', dept: 'DEV'},
    {fname: 'Brad', lname: 'Ullman', empid: '2101', email: 'brad.ullman@concur.com', location: 'Bellevue', dept: 'DEV'},
    {fname: 'Darin', lname: 'Warling', empid: '2301', email: 'darin.warling@concur.com', location: 'Minneapolis (remote)', dept: 'DEV'}
];

const mruData = data.slice(2, 5);

const filterData = [
    {text: 'All', id: '0', filterCode: '', active: true},
    {text: 'Development', id: '1', filterCode: 'DEV'},
    {text: 'Quality Assurance', id: '2', filterCode: 'QA'},
    {text: 'Project Management', id: '3', filterCode: 'PM'}
];

class AutoCompleteFilterExample extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            // If no default filter has been specified, automatically pick the first one in the list:
            activeFilterItemId: ( this.props.activeFilterItemId ? this.props.activeFilterItemId : filterData[0].id ),

            data: data,
            mruData: mruData,
            filterData: (this.props.loadFilterDataAfterInitialRender ? [] : this.props.filterData || filterData),
            searchText: (this.props.value ? this.props.value : ''),
            filter: ''
        };
    }

    componentDidMount() {
        if (this.props.loadFilterDataAfterInitialRender) {
            // this is an example to show `filterData` being applied after the initial render
            // so we are disabling the lint rule that says not to setState in componentDidMount
            this.setState({ // eslint-disable-line
                filterData: this.props.filterData || filterData
            });
        }
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.value !== this.state.searchText) {
            this.setState({
                searchText: nextProps.value
            });
        }
    }

    applyFilter = (itemData) => {
        return this.state.filter ? this.state.filter === itemData.dept : true;
    }

    filterByText = (text, itemData) => {
        if (!text) {
            return true;
        } else if (itemData.fname.toLowerCase().indexOf(text.toLowerCase()) >= 0 ||
            itemData.lname.toLowerCase().indexOf(text.toLowerCase()) >= 0 ||
            itemData.empid.toLowerCase().indexOf(text.toLowerCase()) >= 0 ||
            itemData.email.toLowerCase().indexOf(text.toLowerCase()) >= 0 ||
            itemData.location.toLowerCase().indexOf(text.toLowerCase()) >= 0) {
            return true;
        }

        return false;
    }

    handleChange = (text) => {
        this.setState({
            searchText: text,
            data: data.filter(rec => {
                return this.filterByText(text, rec) && this.applyFilter(rec);
            }),
            mruData: mruData.filter(rec => {
                return this.filterByText(text, rec) && this.applyFilter(rec);
            })
        });
    }

    handleFilterSelect = (item) => {
        if (item) {
            const rec = getRecordById(this.state.filterData, item.props.id);
            if (rec) {
                this.setState({
                    activeFilterItemId: item.props.id,
                    filter: rec.filterCode
                }, () => {
                    this.handleChange(this.state.searchText);
                });
            }
        }
    }

    handleSelect = (itemData) => {
        this.setState({
            searchText: this._renderInputText(itemData),
            data: data.filter(rec => {
                return rec.empid === itemData.empid;
            }),
            mruData: mruData.filter(rec => {
                return rec.empid === itemData.empid;
            })
        });
    }

    _renderInputText = (itemData) => {
        return itemData.lname + ', ' + itemData.fname;
    }

    render() {

        const {
            loadFilterDataAfterInitialRender,
            ...otherProps
        } = this.props;

        return (
            <AutoCompleteFilter
                {...otherProps}
                activeFilterItemId={this.state.activeFilterItemId}
                data={this.state.data}
                dataRendering={{
                    inputRenderer: this._renderInputText,
                    listItemRenderer: (itemData) => {
                        return <Employee {...itemData} />;
                    }
                }}
                disabled
                dropdownTitle='filter'
                filterData={this.state.filterData}
                itemKey={'empid'}
                mruData={this.state.mruData}
                mruTitle='Most Recently Used'
                onChange={(e) => this.handleChange(e.target.value)}
                onFilterSelect={this.handleFilterSelect}
                onSelect={this.handleSelect}
                value={this.state.searchText} />
        );
    }
}

ReadOnly State

ReadOnly is not a supported state for the AutocompleteFilter component.

Usage

Properties

Property Type Default Description
noResultsMsg String or Object Required Displayed when no data items match entered text. If a string is used, it will display that text. If an object is used, it has props for title (primary text) and description (secondary text).
active String   Id of the active data item.
activeFilterItemId String   Id of the active filterData item.
className String   Custom classes to be added to the <input> tag.
data Array   Raw data for the dropdown items. Each element in the array (in JSON format) represents an item in the dropdown. For default rendering of list items, each item should have keys for id and text. For custom rendering of list items, see dataRendering.
dataRendering Object   Enables custom rendering. See dataRendering for details about the available properties.
disabled Boolean false Controls whether the component is enabled or disabled.
dropdownIcon String 'filter' Dropdown icon class name. Populating this field displays an icon on the dropdown button instead of the dropdownTitle label.
dropdownTitle String   Dropdown button label. If dropdownIcon is defined this becomes an accessible ARIA label to identify the button.
dropdownTriggerLabel String Required Localized assistive text for the dropdown trigger button.
filterButtonProps Object   Properties to pass through to the filter button.
filterData Array   List of possible filter selections. Array of objects with properties for id and text; e.g., { { id: 1, text: 'Filter 1' }, { id: 2, text: 'Filter 2' } }.
filterSearchInputProps Object   Properties to pass through to the filter dropdown search field.
infinite Object   Defines the properties for infinite scrolling. See infinite for more details.
isLoading Boolean false When true, loadingMsg will be displayed.
isOpen Boolean false When true, the input dropdown will be opened automatically on creation.
itemKey String or Function 'id' Used to identify the unique key for a list item. If a string is used, it identifies the field in the data to use as the key value for the list item. If a function is used, it must return the key value for the list item. The function will be passed the row of data (in JSON format).
loadingMsg Node   Displayed while data is loading. Must be non-empty if isLoading is true.
mruData Array   Raw data for the most-recently used (MRU) items (in JSON format). For simple rendering of list items, each item should have keys for id and text. For custom rendering of list items, see dataRendering.
mruTitle String   Title of MRU section inside dropdown menu.
placeholder String   Placeholder displayed when input field is empty.
popperClassName String   Custom classes to be added to the popper <div> element.
popperPlacement String or String[]   Placement of the dropdown relative to the input element. Options are 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end'. See Popover Placement for a more detailed description of the values.
required Boolean false Adds the required attribute to the <input> element. NOTE: This does not perform any validation, but rather just marks the field as required.
size String 'lg' Component size. Options are 'lg', 'md' and 'sm'.
showAllOnFocus Boolean true If true, dropdown menu opens automatically when component receives focus. If false, dropdown menu will not open until at least one character is entered (or more, depending on the value of showAllOnFocusThreshold.
showAllOnFocusThreshold Number 0 Minimum number of characters that must be entered before the autocomplete menu is displayed.
showFilterSearch Boolean false When true, a field is displayed in the filter dropdown to allow searching within filterData.
targetClassName String   Custom classes to be added to the target/reference <div> wrapper element.
value String   Initial (default) value of input field.
widthSizingType String 'minTarget' The width of the popper component relative to the target. Options are 'none','matchTarget','minTarget','maxTarget'

Callbacks

Property Parameters Description
onBlur event Callback function for input field onBlur events. Will be called only if input field loses focus and the Dropdown popover is closed.
onChange event Callback function for input field onChange events.
onDropdownClose event Callback function for onDropdownClose events.
onFilterSelect item Callback function for onFilterSelect events.
onFocus event Callback function for input field onFocus events.
onKeyDown event Callback function for input field onKeyDown events.
onMouseDown event Callback function for input field onMouseDown.
onSelect item Callback function for input field onSelect events.

Shape: dataRendering

Properties

Property Type Default Description
inputRenderer String or Function 'text' Used to render text in the input for a selected item. If a string is used, the value for that key (from data or mruData) will be shown. If a function is used, it must return the text to display in the input field. The function will be passed the row of data (in JSON format).
listItemRenderer String or Function 'text' Used to render content for each list item in the dropdown. If a string is used, the value for that key (from data or mruData) will be shown. If a function is used, it must return a valid node. The function will be passed the row of data (in JSON format).

Shape: infinite

Properties

Property Type Default Description
hasMore Boolean true Sets whether there are more items to be loaded. Event listeners are removed if set to false. This can also be used to disable infinite loading. NOTE: Setting hasMore to true emits a loading message. Having bothisLoading and hasMore set to true will result in multiple loading messages.
isInitialLoad Boolean true Sets whether the component should load the first set of items when opened.
pageStart Number 0 The number of the first page to load. With the default of 0, the first page is loaded.
threshold Number 250 The distance in pixels from the bottom of the scroll area before a call to onLoadMore is triggered.

Callbacks

Property Parameters Description
onLoadMore pageNumber A callback when more items are requested by the user. Provides a single parameter specifying the page to load.