diff --git a/concepts/05 UI Components/DropDownBox/15 Synchronize with the Embedded Element.md b/concepts/05 UI Components/DropDownBox/15 Synchronize with the Embedded Element.md index d259d2381b..b81b16d704 100644 --- a/concepts/05 UI Components/DropDownBox/15 Synchronize with the Embedded Element.md +++ b/concepts/05 UI Components/DropDownBox/15 Synchronize with the Embedded Element.md @@ -1,7 +1,7 @@ -You should synchronize the DropDownBox UI component with an embedded element. The following instructions show how to do it when the embedded element is another DevExtreme UI component, but they are also applicable in other cases. +To use DropDownBox with an embedded UI component, synchronize the two components' data and selection state. The steps below use an embedded DataGrid as an example. The same approach applies to other DevExtreme UI components. 1. **Specify data sources** -The DropDownBox's and embedded UI component's data sources can be the same or different. If they are different, the UI component's key field should be present in the DropDownBox's data source. +The DropDownBox and embedded UI component can have the same data source or different data sources. If the data sources are different, the UI component's key field must be present in the DropDownBox data source. // Different data sources, both have the ID field @@ -16,8 +16,8 @@ The DropDownBox's and embedded UI component's data sources can be the same or di // ... ]; -1. **Specify which data field provides the DropDownBox's values and the embedded UI component's keys** -Assign the field's name to the DropDownBox's [valueExpr](/api-reference/10%20UI%20Components/DataExpressionMixin/1%20Configuration/valueExpr.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#valueExpr') property and to the [key](/api-reference/30%20Data%20Layer/Store/1%20Configuration/key.md '/Documentation/ApiReference/Data_Layer/ArrayStore/Configuration/#key') property of the embedded UI component's store. The following example shows an [ArrayStore](/api-reference/30%20Data%20Layer/ArrayStore '/Documentation/ApiReference/Data_Layer/ArrayStore/'): +1. **Specify the shared key field** +Assign the field's name to the DropDownBox's [valueExpr](/api-reference/10%20UI%20Components/DataExpressionMixin/1%20Configuration/valueExpr.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#valueExpr') property and to the [key](/api-reference/30%20Data%20Layer/Store/1%20Configuration/key.md '/Documentation/ApiReference/Data_Layer/ArrayStore/Configuration/#key') property of the embedded UI component's store. The following example uses an [ArrayStore](/api-reference/30%20Data%20Layer/ArrayStore '/Documentation/ApiReference/Data_Layer/ArrayStore/'): --- ##### jQuery @@ -49,15 +49,23 @@ Assign the field's name to the DropDownBox's [valueExpr](/api-reference/10%20UI% valueExpr="ID" displayExpr="email" [dataSource]="dropDownBoxData"> - - import { DxDropDownBoxModule, DxDataGridModule } from "devextreme-angular"; + import { Component } from "@angular/core"; + import { DxDropDownBoxComponent } from "devextreme-angular/ui/drop-down-box"; + import { DxDataGridComponent } from "devextreme-angular/ui/data-grid"; import ArrayStore from "devextreme/data/array_store"; - // ... + + @Component({ + selector: "app-root", + templateUrl: "./app.component.html", + standalone: true, + imports: [DxDropDownBoxComponent, DxDataGridComponent] + }) export class AppComponent { widgetData: any; dropDownBoxData: any; @@ -70,14 +78,6 @@ Assign the field's name to the DropDownBox's [valueExpr](/api-reference/10%20UI% }); } } - @NgModule({ - imports: [ - // ... - DxDropDownBoxModule, - DxDataGridModule - ], - // ... - }) ##### Vue @@ -88,70 +88,52 @@ Assign the field's name to the DropDownBox's [valueExpr](/api-reference/10%20UI% value-expr="ID" display-expr="email" :data-source="dropDownBoxData"> - - + - ##### React + import React from 'react'; import 'devextreme/dist/css/dx.fluent.blue.light.css'; import { DropDownBox } from 'devextreme-react/drop-down-box'; - import { DataGrid, Selection } from "devextreme-react/data-grid"; - import ArrayStore from "devextreme/data/array_store"; - - const dropDownBoxData = {/* ... */}; + import { DataGrid } from 'devextreme-react/data-grid'; + import ArrayStore from 'devextreme/data/array_store'; + + const dropDownBoxData = [/* ... */]; const gridDataSource = new ArrayStore({ data: widgetData, - key: "ID" + key: 'ID', }); - class App extends React.Component { - render() { - return ( - - - - ); - } + export default function App() { + return ( + + + + ); } - export default App; - ##### ASP.NET MVC Controls @@ -175,9 +157,9 @@ Assign the field's name to the DropDownBox's [valueExpr](/api-reference/10%20UI% --- 1. **Synchronize the DropDownBox's value and the embedded UI component's selection** -This step's implementation depends on the embedded UI component's API and the library/framework you use. If the library/framework supports two-way binding, you can bind the DropDownBox's [value](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/value.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#value') and the UI component's **selectedRowKeys**/**selectedItemKeys** to the same variable. If not, handle events as follows: +The synchronization implementation depends on the embedded UI component's API and the library/framework you use. If the library/framework supports two-way binding, you can bind the DropDownBox's [value](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/value.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#value') and the UI component's **selectedRowKeys**/**selectedItemKeys** to the same variable. If not, handle events as follows: 1. **Set the initial selection in the embedded UI component** - Implement the UI component's **onContentReady** handler to select data items according to the DropDownBox's initial value. In some UI components, you can set the **selectedRowKeys** or **selectedItemKeys** option instead. + Implement the UI component's **onContentReady** handler to select data items according to the DropDownBox's initial value. In some UI components, you can set **selectedRowKeys** or **selectedItemKeys** directly instead of using **onContentReady**. 1. **Update the selection** Implement the DropDownBox's [onValueChanged](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/onValueChanged.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onValueChanged') handler to update the selection when the DropDownBox's value changes. 1. **Update the DropDownBox's value** @@ -191,7 +173,7 @@ This step's implementation depends on the embedded UI component's API and the li $(function() { // ... - const dataGridInstance; + let dataGridInstance; $("#dropDownBox").dxDropDownBox({ // ... value: [1], @@ -217,20 +199,26 @@ This step's implementation depends on the embedded UI component's API and the li ##### Angular - - + - import { DxDropDownBoxModule, DxDataGridModule } from "devextreme-angular"; - import ArrayStore from "devextreme/data/array_store"; - // ... + import { Component } from "@angular/core"; + import { DxDropDownBoxComponent } from "devextreme-angular/ui/drop-down-box"; + import { DxDataGridComponent, DxoDataGridSelectionComponent } from "devextreme-angular/ui/data-grid"; + + @Component({ + selector: "app-root", + templateUrl: "./app.component.html", + standalone: true, + imports: [DxDropDownBoxComponent, DxDataGridComponent, DxoDataGridSelectionComponent] + }) export class AppComponent { - // ... _dropDownBoxValue: number[] = [1]; get dropDownBoxValue(): number[] { return this._dropDownBoxValue; @@ -239,14 +227,6 @@ This step's implementation depends on the embedded UI component's API and the li this._dropDownBoxValue = value || []; } } - @NgModule({ - imports: [ - // ... - DxDropDownBoxModule, - DxDataGridModule - ], - // ... - }) ##### Vue @@ -263,87 +243,51 @@ This step's implementation depends on the embedded UI component's API and the li - ##### React - import React from 'react'; + + import React, { useState, useRef } from 'react'; import 'devextreme/dist/css/dx.fluent.blue.light.css'; - import { DropDownBox } from 'devextreme-react/drop-down-box'; - import { DataGrid, Selection } from "devextreme-react/data-grid"; - import ArrayStore from "devextreme/data/array_store"; - - class App extends React.Component { - constructor(props) { - super(props); - - this.state = { - dropDownBoxValues: [1] - }; - - this.dropDownBoxRef = React.createRef(); - - this.changeDropDownBoxValue = this.changeDropDownBoxValue.bind(this); - } - - changeDropDownBoxValue(e) { - const keys = e.selectedRowKeys; - this.setState({ - dropDownBoxValues: keys - }); - - this.dropDownBoxRef.current.instance().close(); - } - - render() { - return ( - - - - - - ); - } + import { DropDownBox, DropDownBoxRef } from 'devextreme-react/drop-down-box'; + import { DataGrid, Selection } from 'devextreme-react/data-grid'; + import type { SelectionChangedEvent } from 'devextreme/ui/data_grid'; + + export default function App() { + const [dropDownBoxValues, setDropDownBoxValues] = useState([1]); + const dropDownBoxRef = useRef(null); + const changeDropDownBoxValue = (e: SelectionChangedEvent) => { + setDropDownBoxValues(e.selectedRowKeys as number[]); + dropDownBoxRef.current?.instance().close(); + }; + return ( + + + + + + ); } - export default App; - ##### ASP.NET MVC Controls diff --git a/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/00 Search in Embedded Components.md b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/00 Search in Embedded Components.md new file mode 100644 index 0000000000..3ec28506d5 --- /dev/null +++ b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/00 Search in Embedded Components.md @@ -0,0 +1,52 @@ +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 AspectSearch by field values (no lookup column involved)Search by display values (lookup column involved)
Embedded componentDataGrid / TreeListDataGrid / TreeList
User inputText 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 searchDataSource.searchValue(value)DataSource.filter(filterExpr)
Extra data source requiredNoYes, a lookup data source
Filtering logicThe DataSource searches for values in fields specified in searchExprYou 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 closedataSource.searchValue('')dataSource.filter(null)
+ +[note] These implementation strategies support single selection only. To implement multiple selection, use the [TagBox](/api-reference/10%20UI%20Components/dxTagBox '/Documentation/ApiReference/UI_Components/dxTagBox/') component instead. + +For complete working examples, see the following GitHub repositories: + +- [DropDownBox with embedded DataGrid](https://github.com/DevExpress-Examples/devextreme-dropdownbox-filter-data-in-nested-widget) (search by field values) +- [DropDownBox with embedded TreeList](https://github.com/DevExpress-Examples/devextreme-dropdownbox-implement-search-for-treelist) (search by lookup column display value) + +#####See Also##### +- [Search by Field Values (DataGrid)](/concepts/05%20UI%20Components/DropDownBox/20%20Search%20in%20Embedded%20Components/05%20Search%20by%20Field%20Values%20(DataGrid).md '/Documentation/Guide/UI_Components/DropDownBox/Search_in_Embedded_Components/Search_by_Field_Values_(DataGrid)/') +- [Search by Lookup Column (TreeList)](/concepts/05%20UI%20Components/DropDownBox/20%20Search%20in%20Embedded%20Components/10%20Search%20by%20Lookup%20Column%20(TreeList).md '/Documentation/Guide/UI_Components/DropDownBox/Search_in_Embedded_Components/Search_by_Lookup_Column_(TreeList)/') \ No newline at end of file diff --git a/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 Search by Field Values (DataGrid).md b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 Search by Field Values (DataGrid).md new file mode 100644 index 0000000000..f0f128aa52 --- /dev/null +++ b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 Search by Field Values (DataGrid).md @@ -0,0 +1,777 @@ +This topic covers search by field values (no lookup column involved) in a [DropDownBox](/api-reference/10%20UI%20Components/dxDropDownBox '/Documentation/ApiReference/UI_Components/dxDropDownBox/') with an embedded [DataGrid](/api-reference/10%20UI%20Components/dxDataGrid '/Documentation/ApiReference/UI_Components/dxDataGrid/'). The approach uses [`DataSource.searchExpr`](/api-reference/30%20Data%20Layer/DataSource/1%20Configuration/searchExpr.md '/Documentation/ApiReference/Data_Layer/DataSource/Configuration/#searchExpr') and [`DataSource.searchValue`](/api-reference/30%20Data%20Layer/DataSource/3%20Methods/searchValue(value).md '/Documentation/ApiReference/Data_Layer/DataSource/Methods/#searchValuevalue') to filter rows as the user types. + +[note] For a similar example that involves a lookup column, see [Search by Lookup Column (TreeList)](/concepts/05%20UI%20Components/DropDownBox/20%20Search%20in%20Embedded%20Components/10%20Search%20by%20Lookup%20Column%20(TreeList).md '/Documentation/Guide/UI_Components/DropDownBox/Search_in_Embedded_Components/Search_by_Lookup_Column_(TreeList)/'). + +The full working code is available in the GitHub repository: + +#include btn-open-github with { + href: "https://github.com/DevExpress-Examples/devextreme-dropdownbox-filter-data-in-nested-widget" +} + +### 1) Configure DropDownBox to Accept User Input + +- Activate [`acceptCustomValue`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/acceptCustomValue.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#acceptCustomValue') so the user can type text. +- Set [`valueChangeEvent`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/valueChangeEvent.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#valueChangeEvent') to an empty string to prevent the component from trying to interpret typing as a value change. + +--- +##### jQuery + + + $('#gridBox').dxDropDownBox({ + acceptCustomValue: true, + valueChangeEvent: '', + openOnFieldClick: false, + // ... + }); + +##### Angular + + + + + +##### Vue + + + + +##### React + + + + +##### ASP.NET Core Controls + + + @(Html.DevExtreme().DropDownBox() + .AcceptCustomValue(true) + .ValueChangeEvent("") + .OpenOnFieldClick(false) + // ... + ) + +--- + +### 2) Create a `DataSource` That Supports Search + +Use the following `DataSource` members: + +- [`searchExpr`](/api-reference/30%20Data%20Layer/DataSource/1%20Configuration/searchExpr.md '/Documentation/ApiReference/Data_Layer/DataSource/Configuration/#searchExpr') — fields used for searching +- [`searchValue`](/api-reference/30%20Data%20Layer/DataSource/3%20Methods/searchValue(value).md '/Documentation/ApiReference/Data_Layer/DataSource/Methods/#searchValuevalue') — current search string + +--- +##### jQuery + + + 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 + + + @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 + + + 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 + + + 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 + + + @(Html.DevExtreme().DataGrid() + .DataSource(d => d.Mvc().Controller("SampleData").LoadAction("Get").Key("OrderID")) + .DataSourceOptions(op => { op.SearchExpr(new[] { "CustomerName" }); }) + // ... + ) + +--- + +### 3) Configure `displayExpr` + +Use [`displayExpr`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/displayExpr.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#displayExpr') to define how a selected record is displayed in the input: + + 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`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/contentTemplate.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#contentTemplate') to embed a DataGrid. + +To activate focused row and single row selection, specify the following settings: + +- Enable [`focusedRowEnabled`](/api-reference/10%20UI%20Components/dxDataGrid/1%20Configuration/focusedRowEnabled.md '/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/#focusedRowEnabled') to allow keyboard navigation. +- Use [`focusedRowKey`](/api-reference/10%20UI%20Components/dxDataGrid/1%20Configuration/focusedRowKey.md '/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/#focusedRowKey') so search can focus the first match. +- Use single selection: [`selection.mode`](/api-reference/10%20UI%20Components/dxDataGrid/1%20Configuration/selection/mode.md '/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/selection/#mode'). + +--- +##### jQuery + + + contentTemplate: (e, container) => { + const dropDownBox = e.component; + const value = dropDownBox.option('value'); + const $dataGridContainer = $('
'); + 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 + + +
+ + + + + + + + + + + + + +
+ +##### Vue + + + + +##### React + + + + + + + + + + + + + + + + +##### ASP.NET Core Controls + + + @using (Html.DevExtreme().NamedTemplate("EmbeddedDataGridSingle")) + { + @(Html.DevExtreme().DataGrid() + .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`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/onInput.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput') event: + +1. Ensure the dropdown is open +2. Apply `dataSource.searchValue(text)` +3. Load results and move focus to the first match + +--- +##### jQuery + + + 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 + + + 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 + + + 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 + + + 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 + + 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): + + 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`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/onOpened.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#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 + + + 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 + + + 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 + + + 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 + + + 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 + + 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`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/onClosed.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#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 + + + 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 + + + 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 + + + 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 + + + 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 + + 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](/api-reference/10%20UI%20Components/dxTagBox '/Documentation/ApiReference/UI_Components/dxTagBox/') component instead. + +#####See Also##### +- [DropDownBox - Configuration](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/') +- [DropDownBox - onInput](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/onInput.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput') +- [DataGrid - Configuration](/api-reference/10%20UI%20Components/dxDataGrid/1%20Configuration '/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/') +- [DataSource - searchExpr](/api-reference/30%20Data%20Layer/DataSource/1%20Configuration/searchExpr.md '/Documentation/ApiReference/Data_Layer/DataSource/Configuration/#searchExpr') +- [DataSource - searchValue(value)](/api-reference/30%20Data%20Layer/DataSource/3%20Methods/searchValue(value).md '/Documentation/ApiReference/Data_Layer/DataSource/Methods/#searchValuevalue') +- [DevExtreme.AspNet.Data](https://github.com/DevExpress/DevExtreme.AspNet.Data) +- [Search by Lookup Column (TreeList)](/concepts/05%20UI%20Components/DropDownBox/20%20Search%20in%20Embedded%20Components/10%20Search%20by%20Lookup%20Column%20(TreeList).md '/Documentation/Guide/UI_Components/DropDownBox/Search_in_Embedded_Components/Search_by_Lookup_Column_(TreeList)/') diff --git a/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/10 Search by Lookup Column (TreeList).md b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/10 Search by Lookup Column (TreeList).md new file mode 100644 index 0000000000..cf6b790dca --- /dev/null +++ b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/10 Search by Lookup Column (TreeList).md @@ -0,0 +1,889 @@ +This topic covers search by a lookup column display value in a [DropDownBox](/api-reference/10%20UI%20Components/dxDropDownBox '/Documentation/ApiReference/UI_Components/dxDropDownBox/') with an embedded [TreeList](/api-reference/10%20UI%20Components/dxTreeList '/Documentation/ApiReference/UI_Components/dxTreeList/'). 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`](/api-reference/30%20Data%20Layer/DataSource/3%20Methods/filter(filterExpr).md '/Documentation/ApiReference/Data_Layer/DataSource/Methods/#filterfilterExpr') on the main data source. + +[note] For a similar example that does not involve a lookup column, see [Search by Field Values (DataGrid)](/concepts/05%20UI%20Components/DropDownBox/20%20Search%20in%20Embedded%20Components/05%20Search%20by%20Field%20Values%20(DataGrid).md '/Documentation/Guide/UI_Components/DropDownBox/Search_in_Embedded_Components/Search_by_Field_Values_(DataGrid)/'). + +The full working code is available in the GitHub repository: + +#include btn-open-github with { + href: "https://github.com/DevExpress-Examples/devextreme-dropdownbox-implement-search-for-treelist" +} + +### 1) Configure DropDownBox to Accept User Input + + +- Activate [`acceptCustomValue`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/acceptCustomValue.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#acceptCustomValue'). +- Set [`valueChangeEvent`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/valueChangeEvent.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#valueChangeEvent') to an empty string to prevent the component from trying to interpret typing as a value change. + +--- +##### jQuery + + + $('#treeBox').dxDropDownBox({ + acceptCustomValue: true, + valueChangeEvent: '', + openOnFieldClick: false, + // ... + }); + +##### Angular + + + + + +##### Vue + + + + +##### React + + + + +##### ASP.NET Core Controls + + + @(Html.DevExtreme().DropDownBox() + .AcceptCustomValue(true) + .ValueChangeEvent("") + .OpenOnFieldClick(false) + // ... + ) + +--- + +### 2) Create the Main `DataSource` for TreeList + +Declare a data source for the TreeList. + +--- +##### jQuery + + + const dataSource = new DevExpress.data.DataSource({ + store: makeAsyncDataSource('Task_ID', `${url}/Tasks`), + }); + +##### Angular + + + this.dataSource = new DataSource({ + store: AspNetData.createStore({ + key: 'Task_ID', + loadUrl: `${url}/Tasks`, + }), + }); + +##### Vue + + + const dataSource = new DataSource({ + store: AspNetData.createStore({ + key: 'Task_ID', + loadUrl: `${url}/Tasks`, + }), + }); + +##### React + + + const dataSource = new DataSource({ + store: AspNetData.createStore({ + key: 'Task_ID', + loadUrl: `${url}/Tasks`, + }), + }); + +##### ASP.NET Core Controls + + + @(Html.DevExtreme().TreeList() + .DataSource(d => d.Mvc() + .Controller("SampleData") + .LoadAction("GetTasks") + .Key("Task_ID") + ) + // ... + ) + +--- + +### 3) Configure `displayExpr` + +Use [`displayExpr`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/displayExpr.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#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 + + + 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 + + + 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).then((items) => { + this.displayExpr = (item: Task): string => + this.service.getDisplayExpr(item, items); + }); + +##### Vue + + + const lookupItems = ref([]); + + 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 + + + const [lookupItems, setLookupItems] = useState([]); + + 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 + + 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`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/contentTemplate.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#contentTemplate') to render the TreeList. In the TreeList component, activate [`focusedRowEnabled`](/api-reference/10%20UI%20Components/dxTreeList/1%20Configuration/focusedRowEnabled.md '/Documentation/ApiReference/UI_Components/dxTreeList/Configuration/#focusedRowEnabled') and set single [`selection.mode`](/api-reference/10%20UI%20Components/dxTreeList/1%20Configuration/selection/mode.md '/Documentation/ApiReference/UI_Components/dxTreeList/Configuration/selection/#mode'). + +--- +##### jQuery + + + contentTemplate(templateData, container) { + const dropDownInstance = templateData.component; + const value = dropDownInstance.option('value'); + const treeListContainer = $('
').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 + + +
+ + + + + + + + + + + + + + +
+ +##### Vue + + + + +##### React + + + + + + + + + + + + + + + + +##### ASP.NET Core Controls + + + @(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`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/onInput.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#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`](/api-reference/30%20Data%20Layer/DataSource/3%20Methods/filter(filterExpr).md '/Documentation/ApiReference/Data_Layer/DataSource/Methods/#filterfilterExpr') to the main data source: + + 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 + + + onInput(e) { + clearTimeout(searchTimerId); + const instance = e.component; + if (!instance.option('opened')) instance.open(); + searchTimerId = performSearch({ + e, lookupDataSource, dataSource, searchTimeout, searchExprVal, + }); + }, + +##### Angular + + + 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 + + + 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 + + + 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 + + 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): + + 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`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/onOpened.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#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 + + + 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 + + + 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 + + + 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 + + + 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 + + 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`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/onClosed.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#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 + + + 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 + + + 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 + + + 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 + + + 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 + + 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](/api-reference/10%20UI%20Components/dxTagBox '/Documentation/ApiReference/UI_Components/dxTagBox/') component instead. + +### Example + +See this example for more details: [DropDownBox with embedded TreeList](https://github.com/DevExpress-Examples/devextreme-dropdownbox-implement-search-for-treelist). + +#####See Also##### +- [DropDownBox - Configuration](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/') +- [DropDownBox - onInput](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/onInput.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput') +- [TreeList - Configuration](/api-reference/10%20UI%20Components/dxTreeList/1%20Configuration '/Documentation/ApiReference/UI_Components/dxTreeList/Configuration/') +- [DataSource - filter(filterExpr)](/api-reference/30%20Data%20Layer/DataSource/3%20Methods/filter(filterExpr).md '/Documentation/ApiReference/Data_Layer/DataSource/Methods/#filterfilterExpr') +- [DevExtreme.AspNet.Data](https://github.com/DevExpress/DevExtreme.AspNet.Data) +- [Search by Field Values (DataGrid)](/concepts/05%20UI%20Components/DropDownBox/20%20Search%20in%20Embedded%20Components/05%20Search%20by%20Field%20Values%20(DataGrid).md '/Documentation/Guide/UI_Components/DropDownBox/Search_in_Embedded_Components/Search_by_Field_Values_(DataGrid)/')