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

AutoComplete

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 AutoCompletes.
import AutoComplete from '@concur/nui-widgets/lib/AutoComplete/AutoComplete';

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.

AutoComplete shows an expand/collapse toggle button to make the users aware that a list of values exists.

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

Basic Rendering

Basic rendering uses text strings from the data property to render list items and input text. The default is to use the id value for item keys and the text value for display text.

const data = [
    {text: 'Seattle', id: '1', countryCode: 'a'},
    {text: 'Mexico City', id: '2', countryCode: 'b'},
    {text: 'Vancouver', id: '3', countryCode: 'c'},
    {text: 'Minneapolis', id: '4', countryCode: 'a'},
    {text: 'Tijuana', id: '5', countryCode: 'b'},
    {text: 'Toronto', id: '6', countryCode: 'c'}
];

const mruData = [
    {text: 'Seattle', id: '1', countryCode: 'a'},
    {text: 'Mexico City', id: '2', countryCode: 'b'},
    {text: 'Vancouver', id: '3', countryCode: 'c'}
];

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

        this.state = {
            data: data,
            mruData: mruData,
            searchText: (this.props.value ? this.props.value : '')
        };
    }

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

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

        return false;
    }

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

        });
    }

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

    render() {
        return (
            <AutoComplete
                {...this.props}
                data={this.state.data}
                mruData={this.state.mruData}
                mruTitle='Most Recently Used'
                onChange={(e) => this.handleChange(e.target.value)}
                onSelect={this.handleSelect}
                value={this.state.searchText} />
        );
    }
}

Custom Rendering

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

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 = dataUsers.slice(2, 5);

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

        this.state = {
            data: data,
            mruData: mruData,
            searchText: (this.props.value ? this.props.value : '')
        };
    }

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

    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);
            }),
            mruData: mruData.filter(rec => {
                return this.filterByText(text, rec);
            })
        });
    }

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

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

    render() {
        return (
            <AutoComplete
                {...this.props}
                data={this.state.data}
                dataRendering={{
                    inputRenderer: this._renderInputText,
                    listItemRenderer: (itemData) => {
                        return <Employee {...itemData} />;
                    }
                }}
                itemKey={'empid'}
                mruData={this.state.mruData}
                mruTitle='Most Recently Used'
                onChange={(e) => this.handleChange(e.target.value)}
                onSelect={this.handleSelect}
                value={this.state.searchText} />
        );
    }
}

Headers, Dividers and Disabled Items

To include a header, add an object in the data array with the header property set to the text of the header. To include a divider (separator), add an object in the data array with the divider property set to true. To make a list item disabled, add a disabled property set to true to the object in the data array.

const data = [
    {header: '01-Transportation', id: '01'},
    {text: 'Airfare', id: '1', headerId: '01'},
    {text: 'Airfare Fees', id: '2', headerId: '01'},
    {text: 'Car Rental', id: '3', headerId: '01'},
    {text: 'Fuel', id: '4', headerId: '01'},
    {text: 'Parking', id: '5', headerId: '01'},
    {text: 'Taxi', id: '6', headerId: '01', disabled: true},
    {header: '02-Lodging', id: '02'},
    {text: 'Hotel', id: '7', headerId: '02'},
    {text: 'Laundry', id: '8', headerId: '02', disabled: true},
    {header: '03-Meals and Entertainment', id: '03'},
    {text: 'Individual Breakast', id: '9', headerId: '03'},
    {text: 'Individual Lunch', id: '10', headerId: '03'},
    {text: 'Individual Dinner', id: '11', headerId: '03'},
    {divider: true, headerId: '03'},
    {text: 'Business Meal', id: '12', headerId: '03'},
    {text: 'Beverages', id: '13', headerId: '03'},
    {text: 'Entertainment', id: '14', headerId: '03'}
];

const mruData = data.slice(11, 13);

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

        this.state = {
            data: data,
            mruData: mruData,
            searchText: (this.props.value ? this.props.value : '')
        };
    }

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

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

        return false;
    }

    filterHeaders = (filteredData) => {
        let itemsToRemove = [];

        filteredData.forEach(rec => {
            if (!!rec.header) {
                let itemsExist = filteredData.some(rec2 => {
                    return !rec2.divider && rec2.headerId === rec.id;
                });
                if (!itemsExist) {
                    itemsToRemove.push(rec.id);
                }
            }
        });

        if (itemsToRemove.length === 0) {
            return filteredData;
        }

        // now, filter out any unneeded headers and/or dividers
        let updatedData = filteredData.filter(rec => {
            if (!!rec.header && itemsToRemove.indexOf(rec.id) >= 0) {
                return false;
            } else if (!!rec.divider && itemsToRemove.indexOf(rec.headerId) >= 0) {
                return false;
            }

            return true;
        });

        return updatedData;
    }

    handleChange = (text) => {
        let filteredData = data.filter(rec => {
            return this.filterByText(text, rec);
        });

        filteredData = this.filterHeaders(filteredData);

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

        });
    }

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

    render() {
        return (
            <AutoComplete
                {...this.props}
                data={this.state.data}
                mruData={this.state.mruData}
                mruTitle='Most Recently Used'
                onChange={(e) => this.handleChange(e.target.value)}
                onSelect={this.handleSelect}
                value={this.state.searchText} />
        );
    }
}

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 AutocompleteExample = () => (
    <AutoComplete
        isLoading
        loadingMsg={loadingSpinner}
        noResultsMsg='No results found' />
);

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 HIGAutoCompleteEx06 extends React.Component {
constructor(props) {
    super(props);

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

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);

}

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

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

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

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

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

    return (
        <AutoComplete
            {...this.props}
            data={this.state.data}
            infinite={{
                onLoadMore: this.getMoreData,
                hasMore: this.state.hasMore
            }}
            loadingMsg={loadingSpinner}
            onChange={(e) => this.handleChange(e.target.value)}
            onSelect={this.handleSelect}
            value={this.state.searchText} />
    );
}

HIGAutoCompleteEx06.propTypes = AutoComplete.propTypes;

HIGAutoCompleteEx06.defaultProps = {
    noResultsMsg: 'No results found'
};

export default HIGAutoCompleteEx06;

Disabled State

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

const data = [
    {text: 'Seattle', id: '1', countryCode: 'a'},
    {text: 'Mexico City', id: '2', countryCode: 'b'},
    {text: 'Vancouver', id: '3', countryCode: 'c'},
    {text: 'Minneapolis', id: '4', countryCode: 'a'},
    {text: 'Tijuana', id: '5', countryCode: 'b'},
    {text: 'Toronto', id: '6', countryCode: 'c'}
];

const mruData = [
    {text: 'Seattle', id: '1', countryCode: 'a'},
    {text: 'Mexico City', id: '2', countryCode: 'b'},
    {text: 'Vancouver', id: '3', countryCode: 'c'}
];

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

        this.state = {
            data: data,
            mruData: mruData,
            searchText: (this.props.value ? this.props.value : '')
        };
    }

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

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

        return false;
    }

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

        });
    }

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

    render() {
        return (
            <AutoComplete
                {...this.props}
                data={this.state.data}
                disabled
                mruData={this.state.mruData}
                mruTitle='Most Recently Used'
                onChange={(e) => this.handleChange(e.target.value)}
                onSelect={this.handleSelect}
                value={this.state.searchText} />
        );
    }
}

ReadOnly State

ReadOnly is not a supported state for the Autocomplete 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).
dropdownTriggerLabel String Required Localized assistive text for the dropdown trigger button.
active Node   Node of the active 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.
flipContainer Element or Element[]   Used to create a boundary for the underlying Popper to stay within.
infinite Object   Defines the properties for infinite scrolling. See infinite for more details.
inputRef Ref or Function   Attach a ref to the <input> element.
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.
targetClassName String   Custom classes to be added to the target/reference <div> wrapper element.
validationMessage Object   An object identifying a validation message. The object will include properties for state and text; e.g., { state: 'warning', text: 'This is your last warning' }. The options for state are 'error' and 'warning'. text is the message shown in the validation tooltip; as with all text, it must be localized.
validationMessageType String   This will determine how the validation message will appear. Options are 'iconAligned', a tooltip from the warning or error icon, or 'inputAligned', an overlay displaying underneath the input.
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. Will be called only if input field loses focus and the Dropdown popover is closed.
onChange event Callback function for input field onChange.
onDropdownClose event Callback function for onDropdownClose.
onDropdownTriggerClick event Callback function for onClick on the dropdown trigger button.
onFocus event Callback function for input field onFocus.
onKeyDown event Callback function for input field onKeyDown.
onMouseDown event Callback function for input field onMouseDown.
onSelect item Callback function for onSelect.

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.