Angular DropDownBox - Search in Embedded Components

When using DevExtreme DropDownBox with an embedded DataGrid or TreeList component, you may want to allow users to search and filter records in the dropdown. DataGrid and TreeList include a built-in search and filter UI that you can activate. You can also make the DropDownBox input field editable, so that it acts as a search box for the embedded control. In this case, you need to implement custom code, because DropDownBox does not have built-in search functionality. The implementation depends on whether a lookup field is involved in the search.

When to Use Each Approach

Implementation Aspect Search by field values (no lookup column involved) Search by display values (lookup column involved)
Embedded component DataGrid / TreeList DataGrid / TreeList
User input Text that matches values in the main dataset (same record). Text that matches values in the main dataset, a related dataset (lookup display text), or both.
Main API used for search DataSource.searchValue(value) DataSource.filter(filterExpr)
Extra data source required No Yes, a lookup data source
Filtering logic The DataSource searches for values in fields specified in searchExpr You query the lookup data source by display field (for example, Name contains text), map results to keys, then build an OR filter such as [EmployeeID, '=', 1] or [EmployeeID, '=', 5]
Reset search on close dataSource.searchValue('') dataSource.filter(null)
NOTE
These implementation strategies support single selection only. To implement multiple selection, use the TagBox component instead.

For complete working examples, see the following GitHub repositories:

See Also

Search by Field Values (DataGrid)

This topic covers search by field values (no lookup column involved) in a DropDownBox with an embedded DataGrid. The approach uses DataSource.searchExpr and DataSource.searchValue to filter rows as the user types.

NOTE
For a similar example that involves a lookup column, see Search by Lookup Column (TreeList).

The full working code is available in the GitHub repository:

View on GitHub

1) Configure DropDownBox to Accept User Input

  • Activate acceptCustomValue so the user can type text.
  • Set valueChangeEvent to an empty string to prevent the component from trying to interpret typing as a value change.
jQuery
index.js
$('#gridBox').dxDropDownBox({
    acceptCustomValue: true,
    valueChangeEvent: '',
    openOnFieldClick: false,
    // ...
});
Angular
drop-down-grid.component.html
<dx-drop-down-box
    [acceptCustomValue]="true"
    valueChangeEvent=""
    [openOnFieldClick]="false"
    ...
>
</dx-drop-down-box>
Vue
DropDownBoxWithDataGrid.vue
<DxDropDownBox
    :accept-custom-value="true"
    value-change-event=""
    :open-on-field-click="false"
    ...
/>
React
DropDownGrid.tsx
<DropDownBox
    acceptCustomValue={true}
    valueChangeEvent=""
    openOnFieldClick={false}
    ...
/>
ASP.NET Core Controls
Razor C#
@(Html.DevExtreme().DropDownBox()
    .AcceptCustomValue(true)
    .ValueChangeEvent("")
    .OpenOnFieldClick(false)
    // ...
)

2) Create a DataSource That Supports Search

Use the following DataSource members:

jQuery
index.js
const dataSource = new DevExpress.data.DataSource({
    store: DevExpress.data.AspNet.createStore({
        key: 'OrderNumber',
        loadUrl: 'https://js.devexpress.com/Demos/WidgetsGalleryDataService/api/orders',
    }),
    searchExpr: 'Employee',
});
Angular
drop-down-grid.component.ts
app.component.ts
@Input() dataSource!: DataSource;
import AspNetData from 'devextreme-aspnet-data-nojquery';
import { DataSource } from 'devextreme-angular/common/data';

this.dataSource = new DataSource({
    store: AspNetData.createStore({
        key: 'OrderNumber',
        loadUrl: `${url}/Orders`,
    }),
    searchExpr: 'Employee',
});
Vue
DropDownBoxWithDataGrid.vue
import AspNetData from 'devextreme-aspnet-data-nojquery';
import { DataSource } from 'devextreme-vue/common/data';

const dataSource = new DataSource({
    store: AspNetData.createStore({
        key: 'OrderNumber',
        loadUrl: `${url}/Orders`,
    }),
    searchExpr: 'Employee',
});
React
DropDownGrid.tsx
import AspNetData from 'devextreme-aspnet-data-nojquery';
import { DataSource } from 'devextreme-react/common/data';

const dataSource = new DataSource({
    store: AspNetData.createStore({
        key: 'OrderNumber',
        loadUrl: `${url}/Orders`,
    }),
    searchExpr: 'Employee',
});
ASP.NET Core Controls
Razor C#
@(Html.DevExtreme().DataGrid<SampleOrder>()
    .DataSource(d => d.Mvc().Controller("SampleData").LoadAction("Get").Key("OrderID"))
    .DataSourceOptions(op => { op.SearchExpr(new[] { "CustomerName" }); })
    // ...
)

3) Configure displayExpr

Use displayExpr to define how a selected record is displayed in the input:

Code
function displayExpr(item) {
    if (!item || typeof item !== 'object') return '';
    return `${item.Employee}: ${item.StoreState} - ${item.StoreCity} <${item.OrderNumber}>`;
}

4) Configure the Embedded DataGrid in contentTemplate

Configure the DropDownBox component. Use contentTemplate to embed a DataGrid.

To activate focused row and single row selection, specify the following settings:

jQuery
index.js
contentTemplate: (e, container) => {
    const dropDownBox = e.component;
    const value = dropDownBox.option('value');
    const $dataGridContainer = $('<div>');
    container.append($dataGridContainer);
    $dataGridContainer.dxDataGrid({
        dataSource,
        paging: { enabled: true, pageSize: 10 },
        focusedRowEnabled: true,
        focusedRowKey: value,
        autoNavigateToFocusedRow: false,
        remoteOperations: true,
        scrolling: { mode: 'virtual' },
        selection: { mode: 'single' },
        selectedRowKeys: [value],
        height: '100%',
        width: '100%',
        columnAutoWidth: true,
        onContentReady: () => {
            if (!dropDownBox.option('gridFirstLoadCompleted')) {
                dropDownBox.option('gridFirstLoadCompleted', true);
            }
        },
        onSelectionChanged: (args) => {
            if (!args.component.option('resetSelection')) {
                const keys = args.selectedRowKeys;
                dropDownBox.option('value', keys.length ? keys[0] : null);
                dropDownBox.focus();
            }
            args.component.option('resetSelection', false);
        },
        columns: [
            { dataField: 'OrderNumber', caption: 'ID', dataType: 'number' },
            { dataField: 'OrderDate', dataType: 'date', format: 'shortDate' },
            { dataField: 'StoreCity', dataType: 'string' },
            { dataField: 'StoreState', dataType: 'string' },
            { dataField: 'Employee', dataType: 'string' },
            { dataField: 'SaleAmount', dataType: 'number', format: { type: 'currency', precision: 2 } },
        ],
    });
    dataGridInstance = $dataGridContainer.dxDataGrid('instance');
    return container;
},
Angular
drop-down-grid.component.html
<div *dxTemplate="let data of 'content'">
    <dx-data-grid
        #dataGrid
        [dataSource]="dataSource"
        height="100%"
        width="100%"
        [focusedRowEnabled]="true"
        [(focusedRowKey)]="focusedRowKey"
        [(selectedRowKeys)]="selectedRowKeys"
        [autoNavigateToFocusedRow]="false"
        [remoteOperations]="true"
        [columnAutoWidth]="true"
        (onKeyDown)="dataGridKeyDown($event)"
        (onContentReady)="dataGridContentReady($event)"
        (onSelectionChanged)="onSelectionChanged($event)"
    >
        <dxi-data-grid-column dataField="OrderNumber" caption="ID" dataType="number"></dxi-data-grid-column>
        <dxi-data-grid-column dataField="OrderDate" dataType="date"></dxi-data-grid-column>
        <dxi-data-grid-column dataField="StoreState" dataType="string"></dxi-data-grid-column>
        <dxi-data-grid-column dataField="StoreCity" dataType="string"></dxi-data-grid-column>
        <dxi-data-grid-column dataField="Employee" dataType="string"></dxi-data-grid-column>
        <dxi-data-grid-column dataField="SaleAmount" dataType="number">
            <dxo-data-grid-format type="currency" [precision]="2"></dxo-data-grid-format>
        </dxi-data-grid-column>
        <dxo-data-grid-paging [enabled]="true" [pageSize]="10"></dxo-data-grid-paging>
        <dxo-data-grid-selection mode="single"></dxo-data-grid-selection>
        <dxo-data-grid-scrolling mode="virtual"></dxo-data-grid-scrolling>
    </dx-data-grid>
</div>
Vue
DropDownBoxWithDataGrid.vue
<template #default>
    <DxDataGrid
        ref="dataGridRef"
        :data-source="dataSource"
        height="100%"
        width="100%"
        :focused-row-enabled="true"
        v-model:focused-row-key="focusedRowKey"
        :selected-row-keys="selectedRowKeys"
        :auto-navigate-to-focused-row="false"
        :remote-operations="true"
        :column-auto-width="true"
        @key-down="dataGridKeyDown"
        @content-ready="dataGridContentReady"
        @selection-changed="onSelectionChanged"
    >
        <DxColumn data-field="OrderNumber" caption="ID" data-type="number" />
        <DxColumn data-field="OrderDate" data-type="date" />
        <DxColumn data-field="StoreState" data-type="string" />
        <DxColumn data-field="StoreCity" data-type="string" />
        <DxColumn data-field="Employee" data-type="string" />
        <DxColumn data-field="SaleAmount" data-type="number">
            <DxFormat type="currency" :precision="2" />
        </DxColumn>
        <DxPaging :enabled="true" :page-size="10" />
        <DxSelection mode="single" />
        <DxScrolling mode="virtual" />
    </DxDataGrid>
</template>
React
DropDownGrid.tsx
<DataGrid
    ref={dataGridRef}
    dataSource={dataSource}
    height="100%"
    width="100%"
    focusedRowEnabled={true}
    focusedRowKey={selection.focusedRowKey}
    selectedRowKeys={selection.selectedRowKeys}
    autoNavigateToFocusedRow={false}
    remoteOperations={true}
    columnAutoWidth={true}
    onKeyDown={dataGridKeyDown}
    onFocusedRowChanged={onFocusedRowChanged}
    onSelectionChanged={onSelectionChanged}
>
    <Column dataField="OrderNumber" caption="ID" dataType="number" />
    <Column dataField="OrderDate" dataType="date" />
    <Column dataField="StoreState" dataType="string" />
    <Column dataField="StoreCity" dataType="string" />
    <Column dataField="Employee" dataType="string" />
    <Column dataField="SaleAmount" dataType="number">
        <Format type="currency" precision={2} />
    </Column>
    <Paging enabled={true} pageSize={10} />
    <Selection mode="single" />
    <Scrolling mode="virtual" />
</DataGrid>
ASP.NET Core Controls
Razor C#
@using (Html.DevExtreme().NamedTemplate("EmbeddedDataGridSingle"))
{
    @(Html.DevExtreme().DataGrid<SampleOrder>()
        .ID("embedded-datagrid")
        .DataSource(d => d.Mvc().Controller("SampleData").LoadAction("Get").Key("OrderID"))
        .DataSourceOptions(op => { op.SearchExpr(new[] { "CustomerName" }); })
        .Paging(p => p.PageSize(10))
        .FocusedRowEnabled(true)
        .FocusedRowKey(new JS("component.option('value')"))
        .RemoteOperations(true)
        .AutoNavigateToFocusedRow(false)
        .Scrolling(s => s.Mode(GridScrollingMode.Virtual))
        .Selection(s => s.Mode(SelectionMode.Single))
        .SelectedRowKeys(new JS("[component.option('value')]"))
        .Height("100%")
        .Width("100%")
        .ColumnAutoWidth(true)
        .Columns(columns =>
        {
            columns.AddFor(m => m.OrderID).Caption("ID");
            columns.AddFor(m => m.OrderDate).Format("shortDate");
            columns.AddFor(m => m.CustomerName);
            columns.AddFor(m => m.ShipCountry);
            columns.AddFor(m => m.ShipCity);
        })
        .OnSelectionChanged("function(e){ dataGridSelectionChanged(e, component); }")
        .OnKeyDown("dataGridKeyDown")
        .OnContentReady("function(e){ dataGridContentReady(e, component); }")
        .OnInitialized("dataGridInitialized")
    )
}

5) Implement Search in onInput

Handle the DropDownBox onInput event:

  1. Ensure the dropdown is open
  2. Apply dataSource.searchValue(text)
  3. Load results and move focus to the first match
jQuery
index.js
onInput: (e) => {
    clearTimeout(searchTimerId);
    searchTimerId = setTimeout(() => {
        const dropDownBox = e.component;
        if (!dropDownBox.option('opened')) dropDownBox.open();
        const text = dropDownBox.option('text') || '';
        dataSource.searchValue(text);
        if (isSearchIncomplete(dropDownBox)) {
            const onChanged = () => {
                const items = dataSource.items();
                if (items.length > 0) {
                    dataGridInstance.option('focusedRowKey', items[0].OrderNumber);
                }
                dropDownBox.focus();
                dataSource.off('changed', onChanged);
            };
            dataSource.on('changed', onChanged);
            dataSource.load();
        }
    }, searchTimeout);
},
Angular
drop-down-grid.component.ts
onInput(e: DxDropDownBoxTypes.InputEvent): void {
    if (this.searchTimer) clearTimeout(this.searchTimer);
    this.searchTimer = setTimeout(() => {
        if (!this.gridBoxOpened) this.gridBoxOpened = true;
        const text = e.component.option('text');
        this.dataSource.searchValue(text ?? null);
        if (this.isSearchIncomplete(e.component)) {
            const onChanged = (): void => {
                const items = this.dataSource.items();
                if (items.length > 0) {
                    this.focusedRowKey = items[0].OrderNumber;
                }
                this.focusInput();
                this.dataSource.off('changed', onChanged);
            };
            this.dataSource.on('changed', onChanged);
            this.dataSource.load().catch(() => {});
        }
    }, this.searchTimeout);
}
Vue
DropDownBoxWithDataGrid.vue
function onInput(e: DxDropDownBoxTypes.InputEvent): void {
    if (searchTimer.value) clearTimeout(searchTimer.value);
    searchTimer.value = setTimeout(() => {
        if (!gridBoxOpened.value) gridBoxOpened.value = true;
        const text = e.component.option('text');
        props.dataSource.searchValue(text ?? null);
        if (isSearchIncomplete(e.component)) {
            const onChanged = (): void => {
                const items = props.dataSource.items();
                if (items.length > 0) {
                    focusedRowKey.value = (items[0] as OrderItem).OrderNumber;
                }
                dropDownBoxRef.value?.instance?.focus();
                props.dataSource.off('changed', onChanged);
            };
            props.dataSource.on('changed', onChanged);
            props.dataSource.load().catch(() => {});
        }
    }, props.searchTimeout);
}
React
DropDownGrid.tsx
const onChanged = useCallback(() => {
    const items = dataSource.items();
    if (items.length > 0) {
        dispatch({ type: 'SET_FOCUSED_KEY', key: items[0].OrderNumber });
    }
    dropDownBoxRef.current?.instance().focus();
    dataSource.off('changed', onChanged);
}, []);

const onInput = useCallback((e: DropDownBoxTypes.InputEvent) => {
    if (searchTimer.current) clearTimeout(searchTimer.current);
    searchTimer.current = setTimeout(() => {
        if (!gridBoxOpened) setGridBoxOpened(true);
        const text = e.component.option('text');
        dataSource.searchValue(text ?? null);
        if (isSearchIncomplete(e.component)) {
            dataSource.on('changed', onChanged);
            dataSource.load().catch(() => {});
        }
    }, searchTimeout);
}, [dataSource, gridBoxOpened, searchTimeout]);
ASP.NET Core Controls
Code
function onInput(e) {
    clearTimeout(searchTimerId);
    searchTimerId = setTimeout(() => {
        const dropDownBox = e.component;
        if (!dropDownBox.option('opened')) dropDownBox.open();
        performSearch({
            dropDownBox,
            dataSource: getGridDataSource(),
            grid: dataGridInstance,
        });
    }, searchTimeout);
}

6) Detect Whether the User Is Searching (isSearchIncomplete)

The isSearchIncomplete function returns true if the user has changed the input text and a new search must be applied. It compares text (what the user typed) against the component's internal displayValue (the formatted display text of the currently selected value):

Code
function isSearchIncomplete(dropDownBox) {
    let displayValue = dropDownBox.option('displayValue');
    let text = dropDownBox.option('text') || '';
    displayValue = displayValue && displayValue.length && displayValue[0];
    return text !== displayValue;
}

7) Focus Management in onOpened

When the popup opens and the DropDownBox raises its onOpened event, move focus into the DataGrid.

The example implementation waits until the grid is ready (first open) or until the popup animation is complete (subsequent opens), then calls grid.focus().

jQuery
index.js
function onOpened(e) {
    const dropDownBox = e.component;
    const gridFirstLoadCompleted = dropDownBox.option('gridFirstLoadCompleted');
    const handleOptionChanged = (args) => {
        const grid = args.component;
        const triggerCondition = gridFirstLoadCompleted
            ? args.name === 'opened'
            : args.name === 'focusedRowKey' || args.name === 'focusedRowIndex';
        if (triggerCondition) {
            grid.off('optionChanged', handleOptionChanged);
            requestAnimationFrame(() => {
                grid.focus();
                if (gridFirstLoadCompleted) grid.option('opened', false);
            });
        }
    };
    dataGridInstance.on('optionChanged', handleOptionChanged);
    if (gridFirstLoadCompleted) {
        dataGridInstance.option('opened', true);
    }
    const isTextEqualToDisplayValue =
        dropDownBox.option('text') === dropDownBox.option('displayValue')[0];
    const shouldClearSelection =
        (dropDownBox.option('value') && !dropDownBox.option('text')) ||
        !isTextEqualToDisplayValue;
    if (shouldClearSelection && dataGridInstance.option('selectedRowKeys').length) {
        dataGridInstance.option('resetSelection', true);
        dataGridInstance.option('selectedRowKeys', []);
    }
}
Angular
drop-down-grid.component.ts
onOpened(e: DxDropDownBoxTypes.OpenedEvent): void {
    let gridFirstLoadCompleted = this.gridFirstLoadCompleted;
    const dropDownBox = e.component;
    const handleOptionChanged = (args: DxDataGridTypes.OptionChangedEvent): void => {
        const grid = args.component;
        const triggerCondition = gridFirstLoadCompleted
            ? args.name === 'opened'
            : args.name === 'focusedRowKey' || args.name === 'focusedRowIndex';
        if (triggerCondition) {
            grid.off('optionChanged', handleOptionChanged);
            requestAnimationFrame(() => {
                grid.focus();
                grid.option('opened', false);
            });
        }
    };
    this.dataGrid.instance.on('optionChanged', handleOptionChanged);
    if (this.gridFirstLoadCompleted) {
        this.dataGrid.instance.option('opened', true);
    }
    const displayValue = dropDownBox.option('displayValue') as string[];
    const isTextEqualToDisplayValue = dropDownBox.option('text') === displayValue[0];
    const shouldClearSelection =
        (dropDownBox.option('value') && !dropDownBox.option('text')) ||
        !isTextEqualToDisplayValue;
    if (shouldClearSelection && this.selectedRowKeys?.length) {
        this.resetSelection = true;
        this.selectedRowKeys = [];
    }
}
Vue
DropDownBoxWithDataGrid.vue
function onOpened(e: DxDropDownBoxTypes.OpenedEvent): void {
    const _gridFirstLoadCompleted = gridFirstLoadCompleted.value;
    const dropDownBox = e.component;
    function handleOptionChanged(args: DxDataGridTypes.OptionChangedEvent): void {
        const grid = args.component;
        const triggerCondition = _gridFirstLoadCompleted
            ? args.name === 'opened'
            : args.name === 'focusedRowKey' || args.name === 'focusedRowIndex';
        if (triggerCondition) {
            grid.off('optionChanged', handleOptionChanged);
            requestAnimationFrame(() => {
                grid.focus();
                grid.option('opened', false);
            });
        }
    }
    dataGridRef.value?.instance?.on('optionChanged', handleOptionChanged);
    if (gridFirstLoadCompleted.value) {
        dataGridRef.value?.instance?.option('opened', true);
    }
    const { text, value } = dropDownBox.option();
    const displayValue = dropDownBox.option('displayValue') as string[];
    const shouldClearSelection = (value && !text) || text !== displayValue[0];
    if (shouldClearSelection && selectedRowKeys.value?.length) {
        selectedRowKeys.value = [];
    }
}
React
DropDownGrid.tsx
const onOpened = useCallback((e: DropDownBoxTypes.OpenedEvent) => {
    const isFirstLoadComplete = gridFirstLoadCompleted.current;
    const dropDownBox = e.component;
    function handleOptionChanged(args: DataGridTypes.OptionChangedEvent): void {
        const grid = args.component;
        const triggerCondition = isFirstLoadComplete
            ? args.name === 'opened'
            : args.name === 'focusedRowKey' || args.name === 'focusedRowIndex';
        if (triggerCondition) {
            grid.off('optionChanged', handleOptionChanged);
            requestAnimationFrame(() => {
                grid.focus();
                grid.option('opened', false);
            });
        }
    }
    dataGridRef.current?.instance().on('optionChanged', handleOptionChanged);
    if (gridFirstLoadCompleted.current) {
        dataGridRef.current?.instance().option('opened', true);
    }
    const displayValue = dropDownBox.option('displayValue') as string[];
    const shouldClearSelection =
        (dropDownBox.option('value') && !dropDownBox.option('text')) ||
        dropDownBox.option('text') !== displayValue[0];
    if (shouldClearSelection && selection.selectedRowKeys?.length) {
        dispatch({ type: 'RESET' });
    }
}, []);
ASP.NET Core Controls
Code
function onOpened(e) {
    const dropDownBox = e.component;
    const gridFirstLoadCompleted = dropDownBox.option('gridFirstLoadCompleted');
    const handleOptionChanged = (args) => {
        const grid = args.component;
        const triggerCondition = gridFirstLoadCompleted
            ? args.name === 'opened'
            : args.name === 'focusedRowKey' || args.name === 'focusedRowIndex';
        if (triggerCondition) {
            grid.off('optionChanged', handleOptionChanged);
            requestAnimationFrame(() => {
                grid.focus();
                if (gridFirstLoadCompleted) grid.option('opened', false);
            });
        }
    };
    dataGridInstance.on('optionChanged', handleOptionChanged);
    if (gridFirstLoadCompleted) {
        dataGridInstance.option('opened', true);
    }
    const isTextEqualToDisplayValue =
        dropDownBox.option('text') === dropDownBox.option('displayValue')[0];
    const shouldClearSelection =
        (dropDownBox.option('value') && !dropDownBox.option('text')) ||
        !isTextEqualToDisplayValue;
    if (shouldClearSelection && dataGridInstance.option('selectedRowKeys').length) {
        dataGridInstance.option('resetSelection', true);
        dataGridInstance.option('selectedRowKeys', []);
    }
}

8) Reset Component State in onClosed

When the popup closes, the onClosed event restores consistent state if the user typed something but did not confirm a selection.

  • If nothing was loaded, reset the DropDownBox and clear searchValue
  • If a search was in progress (text !== displayValue), auto-select the first row
jQuery
index.js
function onClosed(e) {
    const dropDownBox = e.component;
    const hasLoadedItems = dataGridInstance.getVisibleRows().length;
    const text = dropDownBox.option('text');
    const displayValue = (dropDownBox.option('displayValue') || [])[0];
    if (!hasLoadedItems) {
        dropDownBox.reset(null);
        dataSource.searchValue('');
        dataSource.load();
        return;
    }
    if (text && text !== displayValue) {
        const firstKey = dataGridInstance.getKeyByRowIndex(0);
        dataGridInstance.selectRows(firstKey);
        dataGridInstance.option('focusedRowKey', firstKey);
    }
}
Angular
drop-down-grid.component.ts
onClosed(e: DxDropDownBoxTypes.ClosedEvent): void {
    const dropDownBox = e.component;
    const hasLoadedItems = this.dataGrid.instance.getVisibleRows().length;
    const text = dropDownBox.option('text');
    const displayValue = dropDownBox.option('displayValue') as string[];
    if (!hasLoadedItems) {
        dropDownBox.reset('');
        this.dataSource.searchValue('');
        this.dataSource.load().catch(() => {});
        return;
    }
    if (text && text !== displayValue[0]) {
        const firstKey = this.dataGrid.instance.getKeyByRowIndex(0);
        this.selectedRowKeys = [firstKey];
        this.focusedRowKey = firstKey;
    }
}
Vue
DropDownBoxWithDataGrid.vue
function onClosed(e: DxDropDownBoxTypes.ClosedEvent): void {
    const dropDownBox = e.component;
    const hasLoadedItems = dataGridRef.value?.instance?.getVisibleRows().length;
    const text = dropDownBox.option('text');
    const displayValue = dropDownBox.option('displayValue') as string[];
    if (!hasLoadedItems) {
        dropDownBox.reset('');
        props.dataSource.searchValue('');
        props.dataSource.load().catch(() => {});
        return;
    }
    if (text && text !== displayValue[0]) {
        const firstKey = dataGridRef.value?.instance?.getKeyByRowIndex(0);
        dropDownValue.value = firstKey ?? null;
        selectedRowKeys.value = firstKey ? [firstKey] : [];
        focusedRowKey.value = firstKey ?? null;
    }
}
React
DropDownGrid.tsx
const onClosed = useCallback((e: DropDownBoxTypes.ClosedEvent) => {
    const dropDownBox = e.component;
    const hasLoadedItems = dataGridRef.current?.instance().getVisibleRows().length;
    const text = dropDownBox.option('text');
    const displayValue = dropDownBox.option('displayValue') as string[];
    if (!hasLoadedItems) {
        dropDownBox.reset('');
        dataSource.searchValue('');
        dataSource.load().catch(() => {});
        return;
    }
    if (text && text !== displayValue[0]) {
        const firstKey = dataGridRef.current?.instance().getKeyByRowIndex(0);
        dispatch({ type: 'SELECT_VALUE', value: firstKey });
    }
}, [dataSource]);
ASP.NET Core Controls
Code
function onClosed(e) {
    const dropDownBox = e.component;
    const hasLoadedItems = dataGridInstance.getVisibleRows().length;
    const text = dropDownBox.option('text');
    const displayValue = (dropDownBox.option('displayValue') || [])[0];
    if (!hasLoadedItems) {
        dropDownBox.reset(null);
        dataSource.searchValue('');
        dataSource.load();
        return;
    }
    if (text && text !== displayValue) {
        const firstKey = dataGridInstance.getKeyByRowIndex(0);
        dataGridInstance.selectRows(firstKey);
        dataGridInstance.option('focusedRowKey', firstKey);
    }
}
NOTE
This implementation supports single selection only. To implement multiple selection, use the TagBox component instead.
See Also

Search by Lookup Column (TreeList)

This topic covers search by a lookup column display value in a DropDownBox with an embedded TreeList. Because the search target is a display value from a related dataset, the approach resolves the typed text to matching IDs via the lookup data source, then applies a DataSource.filter on the main data source.

NOTE
For a similar example that does not involve a lookup column, see Search by Field Values (DataGrid).

The full working code is available in the GitHub repository:

View on GitHub

1) Configure DropDownBox to Accept User Input

jQuery
index.js
$('#treeBox').dxDropDownBox({
    acceptCustomValue: true,
    valueChangeEvent: '',
    openOnFieldClick: false,
    // ...
});
Angular
drop-down-list.component.html
<dx-drop-down-box
    [acceptCustomValue]="true"
    valueChangeEvent=""
    [openOnFieldClick]="false"
    ...
>
</dx-drop-down-box>
Vue
DropDownList.vue
<DxDropDownBox
    :accept-custom-value="true"
    value-change-event=""
    :open-on-field-click="false"
    ...
/>
React
DropDownList.tsx
<DropDownBox
    acceptCustomValue={true}
    valueChangeEvent=""
    openOnFieldClick={false}
    ...
/>
ASP.NET Core Controls
Razor C#
@(Html.DevExtreme().DropDownBox()
    .AcceptCustomValue(true)
    .ValueChangeEvent("")
    .OpenOnFieldClick(false)
    // ...
)

2) Create the Main DataSource for TreeList

Declare a data source for the TreeList.

jQuery
index.js
const dataSource = new DevExpress.data.DataSource({
    store: makeAsyncDataSource('Task_ID', `${url}/Tasks`),
});
Angular
app.component.ts
this.dataSource = new DataSource({
    store: AspNetData.createStore({
        key: 'Task_ID',
        loadUrl: `${url}/Tasks`,
    }),
});
Vue
DropDownList.vue
const dataSource = new DataSource({
    store: AspNetData.createStore({
        key: 'Task_ID',
        loadUrl: `${url}/Tasks`,
    }),
});
React
DropDownList.tsx
const dataSource = new DataSource({
    store: AspNetData.createStore({
        key: 'Task_ID',
        loadUrl: `${url}/Tasks`,
    }),
});
ASP.NET Core Controls
Razor C#
@(Html.DevExtreme().TreeList()
    .DataSource(d => d.Mvc()
        .Controller("SampleData")
        .LoadAction("GetTasks")
        .Key("Task_ID")
    )
    // ...
)

3) Configure displayExpr

Use displayExpr to define how a selected item appears in the input field.

Because the displayed text depends on lookup data (employee name from a related dataset), pre-load that data and resolve it in displayExpr:

jQuery
index.js
let lookupItems = [];
const lookupDataSource = makeAsyncDataSource('ID', `${url}/TaskEmployees`);

lookupDataSource.load().then((items) => {
    lookupItems = items;
    $('#treeBox').dxDropDownBox('instance')?.repaint();
});

function displayExpr(item) {
    if (!lookupItems || !lookupItems.length) return 'Loading...';
    if (!item) return '';
    const employeeData = lookupItems.find(
        (employee) => employee.ID === item.Task_Assigned_Employee_ID,
    );
    if (!employeeData) return item.Task_Subject || '';
    return `${employeeData.Name}: ${item.Task_Subject} (${item.Task_Status})`;
}
Angular
app.service.ts
app.component.ts
getDisplayExpr(item: Task, lookupItems: Employee[]): string {
    if (!lookupItems?.length) return 'Loading...';
    if (!item) return '';
    const employee = lookupItems.find(e => e.ID === item.Task_Assigned_Employee_ID);
    if (!employee) return item.Task_Subject || '';
    return `${employee.Name}: ${item.Task_Subject} (${item.Task_Status})`;
}
(this.service.lookupStore.load() as Promise<Employee[]>).then((items) => {
    this.displayExpr = (item: Task): string =>
        this.service.getDisplayExpr(item, items);
});
Vue
DropDownList.vue
const lookupItems = ref<Employee[]>([]);

onMounted(() => {
    lookupStore.load().then((items: Employee[]) => {
        lookupItems.value = items;
        dropDownBoxRef.value?.instance?.repaint();
    });
});

function displayExpr(item: Task): string {
    if (!lookupItems.value.length) return 'Loading...';
    if (!item) return '';
    const employee = lookupItems.value.find(
        (e) => e.ID === item.Task_Assigned_Employee_ID
    );
    if (!employee) return item.Task_Subject || '';
    return `${employee.Name}: ${item.Task_Subject} (${item.Task_Status})`;
}
React
DropDownList.tsx
const [lookupItems, setLookupItems] = useState<Employee[]>([]);

useEffect(() => {
    lookupStore.load().then((items: Employee[]) => {
        setLookupItems(items);
        dropDownBoxRef.current?.instance().repaint();
    });
}, []);

const displayExpr = useCallback((item: Task | null): string => {
    if (!item) return '';
    if (!lookupItems.length) return 'Loading...';
    const employee = lookupItems.find(
        (e) => e.ID === item.Task_Assigned_Employee_ID
    );
    if (!employee) return item.Task_Subject || '';
    return `${employee.Name}: ${item.Task_Subject} (${item.Task_Status})`;
}, [lookupItems]);
ASP.NET Core Controls
Code
function displayExpr(item) {
    if (!lookupItems || !lookupItems.length) return 'Loading...';
    if (!item) return '';
    const employeeData = lookupItems.find(
        (employee) => employee.ID === item.Task_Assigned_Employee_ID,
    );
    if (!employeeData) return item.Task_Subject || '';
    return `${employeeData.Name}: ${item.Task_Subject} (${item.Task_Status})`;
}

4) Configure the Embedded TreeList in contentTemplate

Configure the DropDownBox component. Use contentTemplate to render the TreeList. In the TreeList component, activate focusedRowEnabled and set single selection.mode.

jQuery
index.js
contentTemplate(templateData, container) {
    const dropDownInstance = templateData.component;
    const value = dropDownInstance.option('value');
    const treeListContainer = $('<div>').dxTreeList({
        dataSource,
        hasItemsExpr: 'Has_Items',
        remoteOperations: { filtering: true, sorting: true, grouping: true },
        parentIdExpr: 'Task_Parent_ID',
        columnAutoWidth: true,
        wordWrapEnabled: true,
        showBorders: true,
        height: 400,
        width: '100%',
        focusedRowEnabled: true,
        selection: { mode: 'single' },
        scrolling: { mode: 'virtual' },
        selectedRowKeys: [value],
        focusedRowKey: value,
        onContentReady: () => {
            if (!dropDownInstance.option('listFirstLoadCompleted')) {
                dropDownInstance.option('listFirstLoadCompleted', true);
            }
        },
        onSelectionChanged(args) {
            const { resetSelection } = args.component.option();
            if (!resetSelection) {
                const keys = args.selectedRowKeys;
                dropDownInstance.option('value', keys.length ? keys[0] : null);
                dropDownInstance.focus();
            }
            args.component.option('resetSelection', false);
        },
        columns: [
            { dataField: 'Task_ID' },
            {
                dataField: 'Task_Assigned_Employee_ID',
                caption: 'Employee',
                minWidth: 120,
                lookup: { dataSource: lookupDataSource, valueExpr: 'ID', displayExpr: 'Name' },
            },
            { dataField: 'Task_Subject', width: 300 },
            { dataField: 'Task_Start_Date', caption: 'Start Date', dataType: 'date' },
            { dataField: 'Task_Status', caption: 'Status' },
            { dataField: 'Task_Due_Date', caption: 'Due Date', dataType: 'date' },
        ],
    });
    container.append(treeListContainer);
    treeList = treeListContainer.dxTreeList('instance');
    return container;
},
Angular
drop-down-list.component.html
<div *dxTemplate="let data of 'content'">
    <dx-tree-list
        #treeListRef
        [dataSource]="dataSource"
        hasItemsExpr="Has_Items"
        parentIdExpr="Task_Parent_ID"
        [columnAutoWidth]="true"
        [wordWrapEnabled]="true"
        [showBorders]="true"
        [height]="400"
        [width]="'100%'"
        [focusedRowEnabled]="true"
        [(focusedRowKey)]="focusedRowKey"
        [(selectedRowKeys)]="selectedRowKeys"
        (onContentReady)="treeListOnContentReady($event)"
        (onKeyDown)="treeListOnKeyDown($event)"
        (onSelectionChanged)="onSelectionChanged($event)"
    >
        <dxo-tree-list-remote-operations
            [filtering]="true" [sorting]="true" [grouping]="true">
        </dxo-tree-list-remote-operations>
        <dxo-tree-list-selection mode="single"></dxo-tree-list-selection>
        <dxo-tree-list-scrolling mode="virtual"></dxo-tree-list-scrolling>
        <dxi-tree-list-column dataField="Task_ID"></dxi-tree-list-column>
        <dxi-tree-list-column
            dataField="Task_Assigned_Employee_ID"
            caption="Employee"
            [minWidth]="120"
        >
            <dxo-tree-list-lookup
                [dataSource]="service.lookupStore"
                valueExpr="ID"
                displayExpr="Name"
            ></dxo-tree-list-lookup>
        </dxi-tree-list-column>
        <dxi-tree-list-column dataField="Task_Subject" [width]="300"></dxi-tree-list-column>
        <dxi-tree-list-column dataField="Task_Start_Date" caption="Start Date" dataType="date"></dxi-tree-list-column>
        <dxi-tree-list-column dataField="Task_Status" caption="Status"></dxi-tree-list-column>
        <dxi-tree-list-column dataField="Task_Due_Date" caption="Due Date" dataType="date"></dxi-tree-list-column>
    </dx-tree-list>
</div>
Vue
DropDownList.vue
<template #content>
    <DxTreeList
        ref="treeListRef"
        :data-source="dataSource"
        has-items-expr="Has_Items"
        parent-id-expr="Task_Parent_ID"
        :column-auto-width="true"
        :word-wrap-enabled="true"
        :show-borders="true"
        :height="400"
        width="100%"
        :focused-row-enabled="true"
        v-model:focused-row-key="focusedRowKey"
        v-model:selected-row-keys="selectedRowKeys"
        @content-ready="treeListOnContentReady"
        @key-down="treeListOnKeyDown"
        @selection-changed="onSelectionChanged"
    >
        <DxRemoteOperations :filtering="true" :sorting="true" :grouping="true" />
        <DxSelection mode="single" />
        <DxScrolling mode="virtual" />
        <DxColumn data-field="Task_ID" />
        <DxColumn data-field="Task_Assigned_Employee_ID" caption="Employee" :min-width="120">
            <DxLookup :data-source="lookupStore" value-expr="ID" display-expr="Name" />
        </DxColumn>
        <DxColumn data-field="Task_Subject" :width="300" />
        <DxColumn data-field="Task_Start_Date" caption="Start Date" data-type="date" />
        <DxColumn data-field="Task_Status" caption="Status" />
        <DxColumn data-field="Task_Due_Date" caption="Due Date" data-type="date" />
    </DxTreeList>
</template>
React
DropDownList.tsx
<TreeList
    ref={treeListRef}
    dataSource={dataSource}
    hasItemsExpr="Has_Items"
    parentIdExpr="Task_Parent_ID"
    columnAutoWidth
    wordWrapEnabled
    showBorders
    height={400}
    width="100%"
    focusedRowEnabled
    focusedRowKey={selection.focusedRowKey}
    selectedRowKeys={selection.selectedRowKeys}
    onContentReady={treeListOnContentReady}
    onKeyDown={treeListOnKeyDown}
    onFocusedRowChanged={onFocusedRowChanged}
    onSelectionChanged={onSelectionChanged}
>
    <RemoteOperations filtering sorting grouping />
    <Selection mode="single" />
    <Scrolling mode="virtual" />
    <Column dataField="Task_ID" />
    <Column dataField="Task_Assigned_Employee_ID" caption="Employee" minWidth={120}>
        <Lookup dataSource={lookupStore} valueExpr="ID" displayExpr="Name" />
    </Column>
    <Column dataField="Task_Subject" width={300} />
    <Column dataField="Task_Start_Date" caption="Start Date" dataType="date" />
    <Column dataField="Task_Status" caption="Status" />
    <Column dataField="Task_Due_Date" caption="Due Date" dataType="date" />
</TreeList>
ASP.NET Core Controls
Razor C#
@(Html.DevExtreme().TreeList()
    .DataSource(d => d.Mvc().Controller("SampleData").LoadAction("GetTasks").Key("Task_ID"))
    .ParentIdExpr("Task_Parent_ID")
    .HasItemsExpr("Has_Items")
    .RemoteOperations(r => r.Filtering(true).Sorting(true).Grouping(true))
    .ColumnAutoWidth(true)
    .WordWrapEnabled(true)
    .ShowBorders(true)
    .Height(400)
    .Width("100%")
    .FocusedRowEnabled(true)
    .FocusedRowKey(new JS("component.option('value')"))
    .Selection(s => s.Mode(SelectionMode.Single))
    .SelectedRowKeys(new JS("[component.option('value')]"))
    .Scrolling(s => s.Mode(TreeListScrollingMode.Virtual))
    .Columns(c => {
        c.Add().DataField("Task_ID");
        c.Add().DataField("Task_Assigned_Employee_ID").Caption("Employee").MinWidth(120)
            .Lookup(l => l.DataSource(ds => ds.Mvc().Controller("SampleData").LoadAction("GetEmployees"))
                .ValueExpr("ID").DisplayExpr("Name"));
        c.Add().DataField("Task_Subject").Width(300);
        c.Add().DataField("Task_Start_Date").Caption("Start Date").DataType(GridColumnDataType.Date);
        c.Add().DataField("Task_Status").Caption("Status");
        c.Add().DataField("Task_Due_Date").Caption("Due Date").DataType(GridColumnDataType.Date);
    })
    .OnContentReady("treeListOnContentReady")
    .OnFocusedRowChanged("treeListOnFocusedRowChanged")
    .OnKeyDown("treeListOnKeyDown")
    .OnSelectionChanged("treeListOnSelectionChanged")
    .OnInitialized("treeListOnInitialized")
)

5) Implement Search in onInput

Use the onInput event to open the dropdown and trigger the search. Because the search targets a lookup column display value, the typed text must first be resolved to matching IDs via the lookup data source, then applied as a DataSource.filter to the main data source:

Code
function applySearchFilter(text, lookupField, dataField, searchExprVal, lookupDataSource, dataSource) {
    // Step 1: find employees whose Name contains the typed text
    lookupDataSource.load({ filter: [lookupField, 'contains', text] }).done((items) => {
        const filterParts = [];

        // Step 2: optionally also search in a non-lookup field (for example, Task_Subject)
        if (Array.isArray(searchExprVal)) {
            filterParts.push([searchExprVal[1], 'contains', text]);
        }

        // Step 3: add an OR condition for each matched employee ID
        items.forEach((item, index) => {
            if (filterParts.length > 0 || index > 0) filterParts.push('or');
            filterParts.push([dataField, '=', item.ID]);
        });

        // Step 4: apply the filter (use a "no-results" sentinel if nothing matched)
        const filterExpr = filterParts.length > 0 ? filterParts : [dataField, '=', -1];
        dataSource.filter(filterExpr);
        dataSource.load();
    });
}
jQuery
index.js
onInput(e) {
    clearTimeout(searchTimerId);
    const instance = e.component;
    if (!instance.option('opened')) instance.open();
    searchTimerId = performSearch({
        e, lookupDataSource, dataSource, searchTimeout, searchExprVal,
    });
},
Angular
drop-down-list.component.ts
onInput(e: DxDropDownBoxTypes.InputEvent): void {
    if (this.searchTimerId) clearTimeout(this.searchTimerId);
    const instance = e.component;
    if (!this.dropDownBoxOpened) this.dropDownBoxOpened = true;
    if (this.service.isSearchIncomplete(instance)) {
        const text = instance.option('text');
        if (text) {
            this.searchTimerId = setTimeout(() => {
                this.service.applySearchFilter(text, this.searchExprValue, this.dataSource);
            }, this.searchTimeout);
        } else {
            this.dataSource.filter(null);
        }
        this.focusInput();
    }
}
Vue
DropDownList.vue
function onInput(e: DxDropDownBoxTypes.InputEvent): void {
    if (searchTimerId) clearTimeout(searchTimerId);
    const instance = e.component;
    if (!dropDownBoxOpened.value) dropDownBoxOpened.value = true;
    if (isSearchIncomplete(instance)) {
        const text = instance.option('text');
        if (text) {
            searchTimerId = setTimeout(() => {
                applySearchFilter(text, props.searchExprValue, props.dataSource);
            }, searchTimeoutValue.value);
        } else {
            props.dataSource.filter(null);
        }
        focusInput();
    }
}
React
DropDownList.tsx
const onInput = useCallback((e: DropDownBoxTypes.InputEvent): void => {
    if (searchTimerIdRef.current) clearTimeout(searchTimerIdRef.current);
    const instance = e.component;
    if (!dropDownBoxOpened) setDropDownBoxOpened(true);
    if (isSearchIncomplete(instance)) {
        const text = instance.option('text');
        if (text) {
            searchTimerIdRef.current = setTimeout(() => {
                applySearchFilter(text, searchExprValue, dataSource);
            }, searchTimeout);
        } else {
            dataSource.filter(null);
        }
        focusInput();
    }
}, [searchTimerIdRef, dropDownBoxOpened, searchExprValue, dataSource, searchTimeout]);
ASP.NET Core Controls
Code
function dropDownBoxOnInput(e) {
    clearTimeout(searchTimerId);
    const instance = e.component;
    if (!instance.option('opened')) instance.open();
    const dataSource = treeList.getDataSource();
    searchTimerId = performSearch(e, dataSource);
}

6) Detect Whether the User Is Searching (isSearchIncomplete)

The isSearchIncomplete function returns true if the user has changed the input text and a new search must be applied. It compares text (what the user typed) against the component's internal displayValue (the formatted display text of the currently selected value):

Code
function isSearchIncomplete(dropDownBox) {
    let displayValue = dropDownBox.option('displayValue');
    let text = dropDownBox.option('text') || '';
    displayValue = displayValue && displayValue.length && displayValue[0];
    return text !== displayValue;
}

7) Focus Management in onOpened

When the popup opens and the DropDownBox raises its onOpened event, move focus into the TreeList.

The example implementation waits until the TreeList is ready (first open) or until the popup animation completes (subsequent opens), then calls list.focus(). It also clears the TreeList selection when the input text no longer matches the selected value.

jQuery
index.js
function onOpened(e) {
    const dropDownBox = e.component;
    const listFirstLoadCompleted = dropDownBox.option('listFirstLoadCompleted');
    const handleOptionChanged = (args) => {
        const list = args.component;
        const triggerCondition = listFirstLoadCompleted
            ? args.name === 'opened'
            : args.name === 'focusedRowKey' || args.name === 'focusedColumnIndex';
        if (triggerCondition) {
            list.off('optionChanged', handleOptionChanged);
            requestAnimationFrame(() => {
                list.focus();
                if (listFirstLoadCompleted) list.option('opened', false);
            });
        }
    };
    treeList.on('optionChanged', handleOptionChanged);
    if (listFirstLoadCompleted) {
        treeList.option('opened', true);
    }
    const { text, value } = dropDownBox.option();
    const isTextEqualToDisplayValue = text === dropDownBox.option('displayValue')[0];
    const shouldClearSelection = (value && !text) || !isTextEqualToDisplayValue;
    if (shouldClearSelection && treeList.option('selectedRowKeys').length) {
        treeList.option('resetSelection', true);
        treeList.selectRows([]);
        treeList.pageIndex(0).then(() => {
            treeList.option('focusedRowIndex', 0);
            treeList.option('focusedRowKey', firstRowKey);
        });
    }
}
Angular
drop-down-list.component.ts
onOpened(e: DxDropDownBoxTypes.OpenedEvent): void {
    const treeListInstance = this.treeListRef?.instance;
    const dropDownBox = e.component;
    const handleOptionChanged = (args: DxTreeListTypes.OptionChangedEvent): void => {
        const list = args.component;
        const triggerCondition = this.listFirstLoadCompleted
            ? args.name === 'opened'
            : args.name === 'focusedRowKey' || args.name === 'focusedColumnIndex';
        if (triggerCondition) {
            list.off('optionChanged', handleOptionChanged);
            setTimeout(() => {
                list.focus();
                list.option('opened', false);
            }, 100);
        }
    };
    treeListInstance.on('optionChanged', handleOptionChanged);
    if (this.listFirstLoadCompleted && !this.service.isSearchIncomplete(dropDownBox)) {
        treeListInstance.option('opened', true);
    }
    const { text, value } = dropDownBox.option();
    const displayValue = dropDownBox.option('displayValue') as string[] | undefined;
    const shouldClearSelection = (value && !text) || text !== displayValue?.[0];
    if (shouldClearSelection && this.selectedRowKeys.length) {
        this.resetSelectionFlag = true;
        this.selectedRowKeys = [];
        treeListInstance.pageIndex(0).then(() => {
            this.focusedRowIndex = 0;
            this.focusedRowKey = firstRowKey;
            this.focusInput();
        }).catch(() => {});
    }
}
Vue
DropDownList.vue
function onOpened(e: DxDropDownBoxTypes.OpenedEvent): void {
    const treeListInstance = treeListRef.value?.instance;
    const dropDownBox = e.component;
    function handleOptionChanged(args: DxTreeListTypes.OptionChangedEvent): void {
        const list = args.component;
        const triggerCondition = listFirstLoadCompleted
            ? args.name === 'openTrigger'
            : args.name === 'focusedRowKey' || args.name === 'focusedColumnIndex';
        if (triggerCondition) {
            list.off('optionChanged', handleOptionChanged);
            requestAnimationFrame(() => {
                list.focus();
                list.option('openTrigger', 'closed');
            });
        }
    }
    treeListInstance?.on('optionChanged', handleOptionChanged);
    if (listFirstLoadCompleted) {
        treeListInstance?.option('openTrigger', 'opened');
    }
    const { text, value: dropDownValue } = dropDownBox.option();
    const displayValue = dropDownBox.option('displayValue') as string[] | undefined;
    const shouldClearSelection = (dropDownValue && !text) || text !== displayValue?.[0];
    if (shouldClearSelection) {
        selectedRowKeys.value = [];
        treeListInstance?.pageIndex(0).then(() => {
            treeListInstance?.option('focusedRowIndex', 0);
            focusedRowKey.value = FIRST_ROW_KEY;
            focusInput();
        }).catch(() => {});
    }
}
React
DropDownList.tsx
const onOpened = useCallback((e: DropDownBoxTypes.OpenedEvent): void => {
    const treeListInstance = treeListRef.current?.instance();
    const dropDownBox = e.component;
    function handleOptionChanged(args: TreeListTypes.OptionChangedEvent): void {
        const list = args.component;
        const triggerCondition = listFirstLoadCompletedRef.current
            ? args.name === 'openTrigger'
            : args.name === 'focusedRowKey' || args.name === 'focusedColumnIndex';
        if (triggerCondition) {
            list.off('optionChanged', handleOptionChanged);
            requestAnimationFrame(() => {
                list.focus();
                list.option('openTrigger', 'closed');
            });
        }
    }
    treeListInstance?.on('optionChanged', handleOptionChanged);
    if (listFirstLoadCompletedRef.current) {
        treeListInstance?.option('openTrigger', 'opened');
    }
    const { text, value: dropDownValue } = dropDownBox.option();
    const dropDownDisplayValue = dropDownBox.option('displayValue') as string[] | undefined;
    const shouldClearSelection = (dropDownValue && !text) || text !== dropDownDisplayValue?.[0];
    if (shouldClearSelection) {
        dispatch({ type: 'RESET' });
        treeListInstance?.pageIndex(0).then(() => {
            treeListInstance?.option('focusedRowIndex', 0);
            dispatch({ type: 'SET_FOCUSED_KEY', key: FIRST_ROW_KEY });
            focusInput();
        }).catch(() => {});
    }
}, [focusInput, listFirstLoadCompletedRef, treeListRef]);
ASP.NET Core Controls
Code
function dropDownBoxOnOpened(e) {
    handleDropDownOpened(e);
}

function handleDropDownOpened(e) {
    if (!treeList) return;
    const dropDownBox = e.component;
    const listFirstLoadCompleted = dropDownBox.option('listFirstLoadCompleted');
    const handleOptionChanged = (args) => {
        const list = args.component;
        const triggerCondition = listFirstLoadCompleted
            ? args.name === 'opened'
            : args.name === 'focusedRowKey' || args.name === 'focusedColumnIndex';
        if (triggerCondition) {
            list.off('optionChanged', handleOptionChanged);
            requestAnimationFrame(() => {
                list.focus();
                if (listFirstLoadCompleted) list.option('opened', false);
            });
        }
    };
    treeList.on('optionChanged', handleOptionChanged);
    if (listFirstLoadCompleted) {
        treeList.option('opened', true);
    }
    const { text, value } = dropDownBox.option();
    const isTextEqualToDisplayValue = text === dropDownBox.option('displayValue')[0];
    const shouldClearSelection = (value && !text) || !isTextEqualToDisplayValue;
    if (shouldClearSelection && treeList.option('selectedRowKeys').length) {
        treeList.option('resetSelection', true);
        treeList.selectRows([]);
        treeList.pageIndex(0).then(() => {
            treeList.option('focusedRowIndex', 0);
            treeList.option('focusedRowKey', firstRowKey);
        });
    }
}

8) Reset Component State in onClosed

When the popup closes, the onClosed event restores consistent state if the user typed something but did not confirm a selection. The search state is cleared by calling dataSource.filter(null).

jQuery
index.js
function onClosed(e) {
    const dropDownBox = e.component;
    const { text } = dropDownBox.option();
    const displayValue = dropDownBox.option('displayValue')[0];
    const resetValue = text && text !== displayValue;
    if (!hasLoadedItems) {
        dropDownBox.reset(null);
        dataSource.filter(null);
        dataSource.load();
    }
    if (resetValue && !treeList.option('selectedRowKeys').length) {
        treeList.option('autoSelection', true);
        const firstKey = treeList.getKeyByRowIndex(0);
        treeList.selectRows([firstKey]);
        treeList.option('focusedRowKey', firstKey);
    }
}
Angular
drop-down-list.component.ts
onClosed(e: DxDropDownBoxTypes.ClosedEvent): void {
    const treeListInstance = this.treeListRef?.instance;
    const dropDownBox = e.component;
    const text = dropDownBox.option('text');
    const displayValue = (dropDownBox.option('displayValue') as string[] | undefined)?.[0];
    const resetValue = text && text !== displayValue;
    if (!this.hasLoadedItems) {
        this.value = null;
        this.selectedRowKeys = [];
        this.dataSource.filter(null);
        this.dataSource.load().then(() => {}).catch(() => {});
    }
    if (resetValue && !this.selectedRowKeys.length) {
        this.autoSelectionFlag = true;
        const firstKey = treeListInstance.getKeyByRowIndex(0) as number;
        this.selectedRowKeys = [firstKey];
        this.focusedRowKey = firstKey;
    }
}
Vue
DropDownList.vue
function onClosed(e: DxDropDownBoxTypes.ClosedEvent): void {
    const treeListInstance = treeListRef.value?.instance;
    const dropDownBox = e.component;
    const text = dropDownBox.option('text');
    const displayValue = (dropDownBox.option('displayValue') as string[] | undefined)?.[0];
    const resetValue = text && text !== displayValue;
    if (!hasLoadedItems) {
        value.value = null;
        selectedRowKeys.value = [];
        props.dataSource.filter(null);
        props.dataSource.load().catch(() => {});
    }
    if (resetValue && !selectedRowKeys.value.length && treeListInstance) {
        const firstKey = treeListInstance.getKeyByRowIndex(0) as number;
        value.value = firstKey;
        selectedRowKeys.value = [firstKey];
        focusedRowKey.value = firstKey;
    }
}
React
DropDownList.tsx
const onClosed = useCallback((e: DropDownBoxTypes.ClosedEvent): void => {
    const treeListInstance = treeListRef.current?.instance();
    const dropDownBox = e.component;
    const text = dropDownBox.option('text');
    const dropDownDisplayValue = (dropDownBox.option('displayValue') as string[] | undefined)?.[0];
    const resetValue = text && text !== dropDownDisplayValue;
    if (!hasLoadedItemsRef.current) {
        dispatch({ type: 'SELECT_VALUE', value: null });
        dataSource.filter(null);
        dataSource.load().catch(() => {});
    }
    if (resetValue && !selection.selectedRowKeys.length && treeListInstance) {
        const firstKey = treeListInstance.getKeyByRowIndex(0) as number;
        dispatch({ type: 'SELECT_VALUE', value: firstKey });
    }
}, [selection.selectedRowKeys.length, dataSource]);
ASP.NET Core Controls
Code
function dropDownBoxOnClosed(e) {
    const dataSource = treeList.getDataSource();
    resetSearchState(e, dataSource);
}

function resetSearchState(e, dataSource) {
    if (!treeList) return;
    const dropDownBox = e.component;
    const { text } = dropDownBox.option();
    const displayValue = dropDownBox.option('displayValue')[0];
    const resetValue = text && text !== displayValue;
    if (!hasLoadedItems) {
        dropDownBox.reset(null);
        dataSource.filter(null);
        dataSource.load();
    }
    if (resetValue && !treeList.option('selectedRowKeys').length) {
        treeList.option('autoSelection', true);
        const firstKey = treeList.getKeyByRowIndex(0);
        treeList.selectRows([firstKey]);
        treeList.option('focusedRowKey', firstKey);
    }
}
NOTE
This implementation supports single selection only. To implement multiple selection, use the TagBox component instead.

Example

See this example for more details: DropDownBox with embedded TreeList.

See Also