From 8e796480c1dc616ad9981d7ac53700ae4d6e6ada Mon Sep 17 00:00:00 2001 From: Vlada Skorokhodova Date: Wed, 10 Jun 2026 11:31:00 +0400 Subject: [PATCH 01/13] DropDownBox: add Search in Embedded Components topic (WIP) --- ...5 Synchronize with the Embedded Element.md | 236 ++---- .../00 Search in Embedded Components.md | 715 ++++++++++++++++ .../05 DataGrid.md | 768 ++++++++++++++++++ .../10 TreeList.md | 173 ++++ 4 files changed, 1746 insertions(+), 146 deletions(-) create mode 100644 concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/00 Search in Embedded Components.md create mode 100644 concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 DataGrid.md create mode 100644 concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/10 TreeList.md 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..d1611d5dd0 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's and embedded UI component's data sources can be the same or different. If they are different, the UI component's key field must be present in the DropDownBox's 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,36 +88,23 @@ Assign the field's name to the DropDownBox's [valueExpr](/api-reference/10%20UI% value-expr="ID" display-expr="email" :data-source="dropDownBoxData"> - - + - ##### React @@ -126,32 +113,26 @@ Assign the field's name to the DropDownBox's [valueExpr](/api-reference/10%20UI% 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 +156,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 +172,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 +198,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 +226,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 +242,52 @@ 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..9e0d6831fd --- /dev/null +++ b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/00 Search in Embedded Components.md @@ -0,0 +1,715 @@ +DropDownBox does not support filtering on its own because it has no built-in content. To add search functionality, embed a component that supports data filtering. DataGrid and TreeList are the most suitable options when you need partial data loading. Both components offer multiple ways to filter content: + +- [Searching API](https://js.devexpress.com/jQuery/Documentation/Guide/Data_Binding/Data_Layer/#Reading_Data/Search_Api) +- [Filtering API](https://js.devexpress.com/jQuery/Documentation/Guide/UI_Components/DataGrid/Filtering_and_Searching/#API/Initial_and_Runtime_Filtering) + +This article describes how to implement search functionality when a DataGrid or TreeList is embedded inside the DropDownBox, and explains the key differences between the two approaches. + +You can use the following GitHub examples as a starting point: + +- [DropDownBox with embedded DataGrid](https://github.com/DevExpress-Examples/devextreme-dropdownbox-filter-data-in-nested-widget) +- [DropDownBox with embedded TreeList](https://github.com/DevExpress-Examples/devextreme-dropdownbox-implement-search-for-treelist) + +## General Configuration + +Both implementations share the same general structure. The sections below describe each part. + +Enable `acceptCustomValue` and set `valueChangeEvent` to an empty string to handle user input. Both examples use [DevExtreme.AspNet.Data](https://github.com/DevExpress/DevExtreme.AspNet.Data) as a data source. + +### displayExpr + +displayExpr defines what text appears in the DropDownBox input field when a value is selected. For example: + + function displayExpr(item) { + if (!item || typeof item !== 'object') return ''; + return `${item.Employee}: ${item.StoreState} - ${item.StoreCity} <${item.OrderNumber}>`; + } + + +To display a value from a lookup column in the DropDownBox input field, pre-load the lookup 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.length) return 'Loading...'; + 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.component.ts + (this.service.lookupStore.load() as Promise).then((items) => { + this.displayExpr = (item: Task): string => + this.service.getDisplayExpr(item, items); + }); + + // app.service.ts + getDisplayExpr(item: Task, lookupItems: Employee[]): string { + 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})`; + } + +##### Vue + + // DropDownList.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...'; + 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([]); + + 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]); + +--- + +Why the difference? DataGrid displays flat data where all fields are stored in the same record. TreeList in this example uses a lookup column where the display value (Name) comes from a related dataset. When the DropDownBox needs to format the selected value, it can only access the raw record — so employee names must be pre-loaded and resolved manually. + +### onInput + +Both implementations debounce the input with setTimeout and open the dropdown if it is closed. After that, the search logic diverges. + +In DataGrid, `dataSource.searchValue(text)` is used which works together with the configured `searchExpr`: + +--- +##### 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].OrderNumber; + } + dropDownBoxRef.value?.instance?.focus(); + props.dataSource.off('changed', onChanged); + }; + props.dataSource.on('changed', onChanged); + props.dataSource.load().catch(() => {}); + } + }, props.searchTimeout); + } + +##### React + + 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); + } + +--- + +In TreeList, since the lookup column is used, the lookup datasource is queried first to resolve matching IDs, which are then assembled into a filter expression applied to the main DataSource: + +--- +##### 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]); + +--- +The `applySearchFilter` function (called inside `performSearch`): + + 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 (e.g., 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" filter if nothing matched) + const filterExpr = filterParts.length > 0 ? filterParts : [dataField, '=', -1]; + dataSource.filter(filterExpr); + dataSource.load(); + }); + } + +The `searchValue` / `searchExpr` work on the fields stored in the dataset. When a column is a lookup (the displayed value comes from a related table), the main dataset only contains the raw ID. There is no built-in way to search by the resolved display text — so the lookup must be queried separately and its results converted to an ID-based filter. + +### isSearchIncomplete + +The `isSearchIncomplete` function returns `true` if the user has changed the input text and a new search needs to be applied (i.e., `text` differs from the current `displayValue`): + + function isSearchIncomplete(dropDownBox) { + let displayValue = dropDownBox.option('displayValue'); + let text = dropDownBox.option('text') || ''; + displayValue = displayValue && displayValue.length && displayValue[0]; + return text !== displayValue; + } + +### onOpened + +The onOpened handler is called each time the drop-down popup opens. Its responsibilities are: + +- Move focus into the inner widget when opened +- Clear the inner widget's selection when the DropDownBox input value has been reset + +Both implementations share the same logic structure. The following example shows the DataGrid implementation: + +--- +##### 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 { + const 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 (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', []); + } + } + +--- + +Why `requestAnimationFrame`? The `onOpened` event fires when the popup starts opening, but the animation may not be complete yet. Calling `.focus()` before the popup is fully visible can cause the browser to scroll unexpectedly or silently fail. `requestAnimationFrame` defers execution to the next paint frame, by which time the popup is fully visible and its content is rendered. + +### onClosed + +The onClosed handler fires when the drop-down popup closes — whether a user selected a value, pressed Escape, clicked outside, or simply dismissed it without confirming. Its job is to restore the DropDownBox and the inner widget to a consistent state when the popup closes without a confirmed selection. + +The core check in both implementations compares `text` (what is currently visible in the input) against `displayValue` (the formatted representation of the currently selected value). If they differ, it means the user typed something that did not result in a confirmed selection — either no match was found, or the user dismissed the popup mid-search: + +--- +##### 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 approach supports single selection only. To implement multiple selection, use the [TagBox](/api-reference/10%20UI%20Components/dxTagBox '/Documentation/ApiReference/UI_Components/dxTagBox/') component instead. + + +## Summary: Search Implementation Depends on Whether You Search a Lookup Column + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AspectSearch by regular field (no lookup column involved)Search by lookup column display value (lookup involved)
Typical componentDataGrid / TreeListDataGrid / TreeList
What user typesText that exists in the main dataset (same record).Text that exists in the main dataset or/and text that exists in a related dataset (lookup display value)
Main API used for searchDataSource.searchValue(value)DataSource.filter(filterExpr)
Search fields configurationsearchExpr specifies the field(s) to searchsearchExpr is not enough for lookup display text; you must build an ID-based filter
Extra datasource requiredNoYes (lookup datasource / store to resolve typed text → matching IDs)
Filtering logicThe DataSource performs searching internally using searchExpr + searchValueYou query the lookup datasource by display field (for example, Name contains text), map results to keys, then build an OR filter like [EmployeeID, '=', 1] or [EmployeeID, '=', 5]
Reset search on closedataSource.searchValue('')dataSource.filter(null)
+ + + +## See Also + +- [DropDownBox — Configuration](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/) +- [DropDownBox — `onInput`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput) +- [DataSource — `searchValue`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Methods/#searchValuevalue) +- [DataSource — `searchExpr`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Configuration/#searchExpr) +- [DataSource — `filter`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Methods/#filterfilterExpr) +- [DevExtreme.AspNet.Data](https://github.com/DevExpress/DevExtreme.AspNet.Data) +- [DropDownBox with embedded DataGrid — Example](https://github.com/DevExpress-Examples/devextreme-dropdownbox-filter-data-in-nested-widget) +- [DropDownBox with embedded TreeList — Example](https://github.com/DevExpress-Examples/devextreme-dropdownbox-implement-search-for-treelist) diff --git a/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 DataGrid.md b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 DataGrid.md new file mode 100644 index 0000000000..56573affa4 --- /dev/null +++ b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 DataGrid.md @@ -0,0 +1,768 @@ +By default, the [DropDownBox](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/) component does not support filtering because it does not contain built-in content. This topic shows how to implement search when a [DataGrid](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDataGrid/) is embedded inside the DropDownBox. + +(You can insert a GIF here to demonstrate the final behavior) + +**Note:** This example demonstrates how to search when DataGrid has a plain data structure. If you need to implement search through a lookup column, see the TreeList example: [DropDownBox with embedded TreeList](https://github.com/DevExpress-Examples/devextreme-dropdownbox-implement-search-for-treelist). + +## 1) Configure DropDownBox to accept user input + +- Enable [`acceptCustomValue`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#acceptCustomValue) so the user can type text. +- Set [`valueChangeEvent`](https://js.devexpress.com/jQuery/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 + + // DropDownGrid.tsx + + +##### ASP.NET Core Controls + + @(Html.DevExtreme().DropDownBox() + .AcceptCustomValue(true) + .ValueChangeEvent("") + .OpenOnFieldClick(false) + // ... + ) + +--- + +## 2) Create a `DataSource` that supports search + +Search is implemented with: + +- [`searchExpr`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Configuration/#searchExpr) — fields used for searching +- [`searchValue`](https://js.devexpress.com/jQuery/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 + + // drop-down-grid.component.ts + @Input() dataSource!: DataSource; + + // app.component.ts + 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 + + @(Html.DevExtreme().DataGrid() + .DataSource(d => d.Mvc().Controller("SampleData").LoadAction("Get").Key("OrderID")) + .DataSourceOptions(op => { op.SearchExpr(new[] { "CustomerName" }); }) + // ... + ) + +--- + +## 3) Configure `displayExpr` + +Use [`displayExpr`](https://js.devexpress.com/jQuery/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) Implement search in `onInput` + +Use the DropDownBox [`onInput`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput) event to: +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 + + // 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 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); + } + +--- + +--- + +## 5) Detect whether the user is currently searching (`isSearchIncomplete`) + +This helper compares: +- `text` — what the user typed +- `displayValue` — formatted text for the currently selected value + +If they differ, it means the user is searching and the popup state should be managed accordingly. + + function isSearchIncomplete(dropDownBox) { + let displayValue = dropDownBox.option('displayValue'); + let text = dropDownBox.option('text') || ''; + displayValue = displayValue && displayValue.length && displayValue[0]; + return text !== displayValue; + } + +--- +## 6) Configure the embedded DataGrid in `contentTemplate` + +Use DropDownBox [`contentTemplate`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#contentTemplate) to render the DataGrid. + + +To use focused and selection row features, set the following settings: +- Enable [`focusedRowEnabled`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/#focusedRowEnabled) to allow keyboard navigation. +- Use [`focusedRowKey`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/#focusedRowKey) so search can focus the first match. +- Use single selection: [`selection.mode`](https://js.devexpress.com/jQuery/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 + + // DropDownGrid.tsx + + + + + + + + + + + + + + +##### 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") + ) + } + +--- + +## 7) Focus management in `onOpened` + +When the popup opens, move focus into the DataGrid. Use DropDownBox [`onOpened`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onOpened). + +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 + + // 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 + + 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 state in `onClosed` + +When the popup closes, DropDownBox [`onClosed`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onClosed) restores consistent state if the user typed something but did not confirm a selection. + +Typically: +- 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 + + // 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 + + 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); + } + } + +--- +## Example + +See this example for more details: [DropDownBox with embedded DataGrid](https://github.com/DevExpress-Examples/devextreme-dropdownbox-filter-data-in-nested-widget). + +## See Also + +- [DropDownBox](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/) +- [DropDownBox — `onInput`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput) +- [DataGrid](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDataGrid/) +- [DataSource — `searchExpr`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Configuration/#searchExpr) +- [DataSource — `searchValue`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Methods/#searchValuevalue) +- [DevExtreme.AspNet.Data](https://github.com/DevExpress/DevExtreme.AspNet.Data) +- [Example: DropDownBox with embedded DataGrid](https://github.com/DevExpress-Examples/devextreme-dropdownbox-filter-data-in-nested-widget) + + +**Note:** This approach can be used with a single selection. If you want to implement multiple selection, we recommend using TagBox component. diff --git a/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/10 TreeList.md b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/10 TreeList.md new file mode 100644 index 0000000000..2580d4a434 --- /dev/null +++ b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/10 TreeList.md @@ -0,0 +1,173 @@ +By default, the DropDownBox component does not support filtering because it does not contain built-in content. This topic shows how to implement search when a TreeList is embedded inside the DropDownBox. + +You can use the following example as a starting point: DropDownBox with embedded TreeList. + +**Note**: This example demonstrates how to search when the TreeList includes a lookup column and you want the DropDownBox search to work by the lookup display value (for example, employee name), not by the stored key. If you don't need to use a lookup column, see the [DataGrid example](https://github.com/DevExpress-Examples/devextreme-dropdownbox-filter-data-in-nested-widget). + + +### Configure DropDownBox to accept user input + +To allow typing and handle the user input: + +- Enable [`acceptCustomValue`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#acceptCustomValue). +- Set [`valueChangeEvent`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#valueChangeEvent) to an empty string. + +```js +$('#treeBox').dxDropDownBox({ + acceptCustomValue: true, + valueChangeEvent: '', + openOnFieldClick: false, + // ... +}); +``` + +--- + +### Configure `displayExpr` + +Use [`displayExpr`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#displayExpr) to show the selected item in the input. + +If a displayed text depends on lookup data, load that data first and resolve it in `displayExpr`. + +```js +const lookupDataSource = DevExpress.data.AspNet.createStore({ + key: 'ID', + loadUrl: `${url}/TaskEmployees`, +}); + +lookupDataSource.load().then((items) => { + lookupItems = items; + $('#treeBox').dxDropDownBox('instance')?.repaint(); +}); + +function displayExpr(item, lookupItems) { + if (!lookupItems || !lookupItems.length) return 'Loading...'; + + 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})`; +} +``` + +--- + +### Create the main `DataSource` for TreeList + +TreeList search in this scenario is implemented via [`DataSource.filter`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Methods/#filterfilterExpr), because we need to filter by IDs resolved from the lookup datasource. + +```js +const dataSource = new DevExpress.data.DataSource({ + store: DevExpress.data.AspNet.createStore({ + key: 'Task_ID', + loadUrl: `${url}/Tasks`, + }), +}); +``` + +--- + +### Configure the embedded TreeList in `contentTemplate` + +Use DropDownBox [`contentTemplate`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#contentTemplate) to render the TreeList. + +To use the focused row feature, set the following settings: +- Enable [`focusedRowEnabled`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxTreeList/Configuration/#focusedRowEnabled) +- Use [`focusedRowKey`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxTreeList/Configuration/#focusedRowKey) + + +### Implement search in `onInput` (lookup display value scenario) + +Use DropDownBox [`onInput`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput) to: +1. Ensure the dropdown is open +2. Convert typed text into matching lookup IDs +3. Apply an ID-based filter to the main datasource + +```js +onInput(e) { + clearTimeout(searchTimerId); + + const dropDownBox = e.component; + if (!dropDownBox.option('opened')) dropDownBox.open(); + + searchTimerId = performSearch({ + e, + lookupDataSource, + dataSource, + searchTimeout, + searchExprVal, + }); +}, +``` + +The key part is building a filter expression and applying it via [`DataSource.filter`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Methods/#filterfilterExpr): + +```js +function applySearchFilter({ text, lookupField, dataField, searchExprVal, lookupDataSource, dataSource }) { + lookupDataSource.load({ filter: [lookupField, 'contains', text] }).done((items) => { + const filterParts = []; + + // add search for a non-lookup field (for example, subject) + if (Array.isArray(searchExprVal)) { + filterParts.push([searchExprVal[1], 'contains', text]); + } + + // add OR conditions for each resolved lookup ID + items.forEach((item, index) => { + if (filterParts.length > 0 || index > 0) filterParts.push('or'); + filterParts.push([dataField, '=', item.ID]); + }); + + const filterExpr = filterParts.length > 0 ? filterParts : [dataField, '=', -1]; + + dataSource.filter(filterExpr); + dataSource.load(); + }); +} +``` + + +--- + +### Detect whether the user is searching (`isSearchIncomplete`) + +This helper compares: +- `text` — what the user typed +- `displayValue` — formatted text for the currently selected value + +If they differ, it means the user is searching and the popup state should be managed accordingly. + +```js +function isSearchIncomplete(dropDownBox) { + let displayValue = dropDownBox.option('displayValue'); + let text = dropDownBox.option('text') || ''; + displayValue = displayValue && displayValue.length && displayValue[0]; + return text !== displayValue; +} +``` + + +### Focus and selection management (`onOpened` / `onClosed`) + + +Use DropDownBox [`onOpened`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onOpened) to move focus into the TreeList and to clear selection when the input text no longer matches the selected value. + +Use DropDownBox [`onClosed`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onClosed) to restore consistent state when the user typed text but did not confirm a selection. In this topic, the search state is cleared by calling `dataSource.filter(null)`. + +------ + +### 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](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/) +- [DropDownBox — `onInput`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput) +- [TreeList](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxTreeList/) +- [DataSource — `filter`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Methods/#filterfilterExpr) +- [DevExtreme.AspNet.Data](https://github.com/DevExpress/DevExtreme.AspNet.Data) +- [Example: DropDownBox with embedded TreeList](https://github.com/DevExpress-Examples/devextreme-dropdownbox-implement-search-for-treelist) From 29fdeba4f5e8c9bae9c2292ce706b032530f509e Mon Sep 17 00:00:00 2001 From: Vlada Skorokhodova Date: Wed, 10 Jun 2026 18:34:07 +0400 Subject: [PATCH 02/13] Finish working on the articles --- ...5 Synchronize with the Embedded Element.md | 4 +- .../00 Search in Embedded Components.md | 685 +------------- ... 05 Search by Regular Field (DataGrid).md} | 134 +-- .../10 Search by Lookup Column (TreeList).md | 884 ++++++++++++++++++ .../10 TreeList.md | 173 ---- 5 files changed, 971 insertions(+), 909 deletions(-) rename concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/{05 DataGrid.md => 05 Search by Regular Field (DataGrid).md} (78%) create mode 100644 concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/10 Search by Lookup Column (TreeList).md delete mode 100644 concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/10 TreeList.md 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 d1611d5dd0..fdb29069cc 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 @@ -109,6 +109,7 @@ Assign the field's name to the DropDownBox's [valueExpr](/api-reference/10%20UI% ##### React + import React from 'react'; import 'devextreme/dist/css/dx.fluent.blue.light.css'; @@ -259,6 +260,7 @@ The synchronization implementation depends on the embedded UI component's API an ##### React + import React, { useState, useRef } from 'react'; import 'devextreme/dist/css/dx.fluent.blue.light.css'; @@ -269,12 +271,10 @@ The synchronization implementation depends on the embedded UI component's API an 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 ( `; - } - - -To display a value from a lookup column in the DropDownBox input field, pre-load the lookup 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.length) return 'Loading...'; - 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.component.ts - (this.service.lookupStore.load() as Promise).then((items) => { - this.displayExpr = (item: Task): string => - this.service.getDisplayExpr(item, items); - }); - - // app.service.ts - getDisplayExpr(item: Task, lookupItems: Employee[]): string { - 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})`; - } - -##### Vue - - // DropDownList.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...'; - 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([]); - - 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]); - ---- - -Why the difference? DataGrid displays flat data where all fields are stored in the same record. TreeList in this example uses a lookup column where the display value (Name) comes from a related dataset. When the DropDownBox needs to format the selected value, it can only access the raw record — so employee names must be pre-loaded and resolved manually. - -### onInput - -Both implementations debounce the input with setTimeout and open the dropdown if it is closed. After that, the search logic diverges. - -In DataGrid, `dataSource.searchValue(text)` is used which works together with the configured `searchExpr`: - ---- -##### 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].OrderNumber; - } - dropDownBoxRef.value?.instance?.focus(); - props.dataSource.off('changed', onChanged); - }; - props.dataSource.on('changed', onChanged); - props.dataSource.load().catch(() => {}); - } - }, props.searchTimeout); - } - -##### React - - 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); - } - ---- - -In TreeList, since the lookup column is used, the lookup datasource is queried first to resolve matching IDs, which are then assembled into a filter expression applied to the main DataSource: - ---- -##### 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]); - ---- -The `applySearchFilter` function (called inside `performSearch`): - - 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 (e.g., 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" filter if nothing matched) - const filterExpr = filterParts.length > 0 ? filterParts : [dataField, '=', -1]; - dataSource.filter(filterExpr); - dataSource.load(); - }); - } - -The `searchValue` / `searchExpr` work on the fields stored in the dataset. When a column is a lookup (the displayed value comes from a related table), the main dataset only contains the raw ID. There is no built-in way to search by the resolved display text — so the lookup must be queried separately and its results converted to an ID-based filter. - -### isSearchIncomplete - -The `isSearchIncomplete` function returns `true` if the user has changed the input text and a new search needs to be applied (i.e., `text` differs from the current `displayValue`): - - function isSearchIncomplete(dropDownBox) { - let displayValue = dropDownBox.option('displayValue'); - let text = dropDownBox.option('text') || ''; - displayValue = displayValue && displayValue.length && displayValue[0]; - return text !== displayValue; - } - -### onOpened - -The onOpened handler is called each time the drop-down popup opens. Its responsibilities are: - -- Move focus into the inner widget when opened -- Clear the inner widget's selection when the DropDownBox input value has been reset - -Both implementations share the same logic structure. The following example shows the DataGrid implementation: - ---- -##### 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 { - const 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 (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', []); - } - } - ---- - -Why `requestAnimationFrame`? The `onOpened` event fires when the popup starts opening, but the animation may not be complete yet. Calling `.focus()` before the popup is fully visible can cause the browser to scroll unexpectedly or silently fail. `requestAnimationFrame` defers execution to the next paint frame, by which time the popup is fully visible and its content is rendered. - -### onClosed - -The onClosed handler fires when the drop-down popup closes — whether a user selected a value, pressed Escape, clicked outside, or simply dismissed it without confirming. Its job is to restore the DropDownBox and the inner widget to a consistent state when the popup closes without a confirmed selection. - -The core check in both implementations compares `text` (what is currently visible in the input) against `displayValue` (the formatted representation of the currently selected value). If they differ, it means the user typed something that did not result in a confirmed selection — either no match was found, or the user dismissed the popup mid-search: - ---- -##### 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 approach supports single selection only. To implement multiple selection, use the [TagBox](/api-reference/10%20UI%20Components/dxTagBox '/Documentation/ApiReference/UI_Components/dxTagBox/') component instead. - - -## Summary: Search Implementation Depends on Whether You Search a Lookup Column +## When to Use Each Approach @@ -672,22 +16,22 @@ The core check in both implementations compares `text` (what is currently visibl - + - - + + - + - + @@ -701,15 +45,8 @@ The core check in both implementations compares `text` (what is currently visibl
What user types Text that exists in the main dataset (same record).Text that exists in the main dataset or/and text that exists in a related dataset (lookup display value)Text that exists in the main dataset, a related dataset (lookup display value), or both.
Main API used for searchDataSource.searchValue(value)DataSource.filter(filterExpr)DataSource.searchValue(value)DataSource.filter(filterExpr)
Search fields configurationsearchExpr specifies the field(s) to searchsearchExpr specifies the field(s) to search searchExpr is not enough for lookup display text; you must build an ID-based filter
Extra datasource required NoYes (lookup datasource / store to resolve typed text → matching IDs)Yes (lookup datasource / store to resolve typed text → matching IDs)
Filtering logic
+[note] These approaches support 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](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/) -- [DropDownBox — `onInput`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput) -- [DataSource — `searchValue`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Methods/#searchValuevalue) -- [DataSource — `searchExpr`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Configuration/#searchExpr) -- [DataSource — `filter`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Methods/#filterfilterExpr) -- [DevExtreme.AspNet.Data](https://github.com/DevExpress/DevExtreme.AspNet.Data) -- [DropDownBox with embedded DataGrid — Example](https://github.com/DevExpress-Examples/devextreme-dropdownbox-filter-data-in-nested-widget) -- [DropDownBox with embedded TreeList — Example](https://github.com/DevExpress-Examples/devextreme-dropdownbox-implement-search-for-treelist) +#####See Also##### +- [Search by Regular Field (DataGrid)](/concepts/05%20UI%20Components/DropDownBox/20%20Search%20in%20Embedded%20Components/05%20Search%20by%20Regular%20Field%20(DataGrid).md '/Documentation/Guide/UI_Components/DropDownBox/Search_in_Embedded_Components/Search_by_Regular_Field_(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 DataGrid.md b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 Search by Regular Field (DataGrid).md similarity index 78% rename from concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 DataGrid.md rename to concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 Search by Regular Field (DataGrid).md index 56573affa4..429a6be2ab 100644 --- a/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 DataGrid.md +++ b/concepts/05 UI Components/DropDownBox/20 Search in Embedded Components/05 Search by Regular Field (DataGrid).md @@ -1,17 +1,16 @@ -By default, the [DropDownBox](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/) component does not support filtering because it does not contain built-in content. This topic shows how to implement search when a [DataGrid](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDataGrid/) is embedded inside the DropDownBox. +This topic covers search by a regular (non-lookup) field 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. -(You can insert a GIF here to demonstrate the final behavior) +[note] This example uses a plain DataGrid data structure. For search through 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)/'). -**Note:** This example demonstrates how to search when DataGrid has a plain data structure. If you need to implement search through a lookup column, see the TreeList example: [DropDownBox with embedded TreeList](https://github.com/DevExpress-Examples/devextreme-dropdownbox-implement-search-for-treelist). +### 1) Configure DropDownBox to Accept User Input -## 1) Configure DropDownBox to accept user input - -- Enable [`acceptCustomValue`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#acceptCustomValue) so the user can type text. -- Set [`valueChangeEvent`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#valueChangeEvent) to an empty string to prevent the component from trying to interpret typing as a value change. +- Enable [`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: '', @@ -21,7 +20,7 @@ By default, the [DropDownBox](https://js.devexpress.com/jQuery/Documentation/Api ##### Angular - + + @(Html.DevExtreme().DropDownBox() .AcceptCustomValue(true) .ValueChangeEvent("") @@ -61,16 +61,17 @@ By default, the [DropDownBox](https://js.devexpress.com/jQuery/Documentation/Api --- -## 2) Create a `DataSource` that supports search +### 2) Create a `DataSource` That Supports Search Search is implemented with: -- [`searchExpr`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Configuration/#searchExpr) — fields used for searching -- [`searchValue`](https://js.devexpress.com/jQuery/Documentation/ApiReference/Data_Layer/DataSource/Methods/#searchValuevalue) — current search string +- [`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', @@ -81,10 +82,10 @@ Search is implemented with: ##### Angular - // drop-down-grid.component.ts + @Input() dataSource!: DataSource; - // app.component.ts + import AspNetData from 'devextreme-aspnet-data-nojquery'; import { DataSource } from 'devextreme-angular/common/data'; @@ -98,7 +99,7 @@ Search is implemented with: ##### Vue - // DropDownBoxWithDataGrid.vue + import AspNetData from 'devextreme-aspnet-data-nojquery'; import { DataSource } from 'devextreme-vue/common/data'; @@ -112,7 +113,7 @@ Search is implemented with: ##### React - // DropDownGrid.tsx + import AspNetData from 'devextreme-aspnet-data-nojquery'; import { DataSource } from 'devextreme-react/common/data'; @@ -126,6 +127,7 @@ Search is implemented with: ##### ASP.NET Core Controls + @(Html.DevExtreme().DataGrid() .DataSource(d => d.Mvc().Controller("SampleData").LoadAction("Get").Key("OrderID")) .DataSourceOptions(op => { op.SearchExpr(new[] { "CustomerName" }); }) @@ -134,19 +136,19 @@ Search is implemented with: --- -## 3) Configure `displayExpr` +### 3) Configure `displayExpr` -Use [`displayExpr`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#displayExpr) to define how a selected record is displayed in the input: +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) Implement search in `onInput` +### 4) Implement Search in `onInput` + +Use the DropDownBox [`onInput`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/onInput.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput') event to: -Use the DropDownBox [`onInput`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#onInput) event to: 1. Ensure the dropdown is open 2. Apply `dataSource.searchValue(text)` 3. Load results and move focus to the first match @@ -154,6 +156,7 @@ Use the DropDownBox [`onInput`](https://js.devexpress.com/jQuery/Documentation/A --- ##### jQuery + onInput: (e) => { clearTimeout(searchTimerId); searchTimerId = setTimeout(() => { @@ -178,7 +181,7 @@ Use the DropDownBox [`onInput`](https://js.devexpress.com/jQuery/Documentation/A ##### Angular - // drop-down-grid.component.ts + onInput(e: DxDropDownBoxTypes.InputEvent): void { if (this.searchTimer) clearTimeout(this.searchTimer); this.searchTimer = setTimeout(() => { @@ -202,7 +205,7 @@ Use the DropDownBox [`onInput`](https://js.devexpress.com/jQuery/Documentation/A ##### Vue - // DropDownBoxWithDataGrid.vue + function onInput(e: DxDropDownBoxTypes.InputEvent): void { if (searchTimer.value) clearTimeout(searchTimer.value); searchTimer.value = setTimeout(() => { @@ -226,7 +229,16 @@ Use the DropDownBox [`onInput`](https://js.devexpress.com/jQuery/Documentation/A ##### 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(() => { @@ -257,11 +269,10 @@ Use the DropDownBox [`onInput`](https://js.devexpress.com/jQuery/Documentation/A --- ---- - -## 5) Detect whether the user is currently searching (`isSearchIncomplete`) +### 5) Detect Whether the User Is Currently Searching (`isSearchIncomplete`) This helper compares: + - `text` — what the user typed - `displayValue` — formatted text for the currently selected value @@ -274,20 +285,20 @@ If they differ, it means the user is searching and the popup state should be man return text !== displayValue; } ---- -## 6) Configure the embedded DataGrid in `contentTemplate` +### 6) Configure the Embedded DataGrid in `contentTemplate` -Use DropDownBox [`contentTemplate`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#contentTemplate) to render the DataGrid. +Use DropDownBox [`contentTemplate`](/api-reference/10%20UI%20Components/dxDropDownBox/1%20Configuration/contentTemplate.md '/Documentation/ApiReference/UI_Components/dxDropDownBox/Configuration/#contentTemplate') to render the DataGrid. +To use focused and selection row features, specify the following settings: -To use focused and selection row features, set the following settings: -- Enable [`focusedRowEnabled`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/#focusedRowEnabled) to allow keyboard navigation. -- Use [`focusedRowKey`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/#focusedRowKey) so search can focus the first match. -- Use single selection: [`selection.mode`](https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/selection/#mode). +- 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'); @@ -334,7 +345,7 @@ To use focused and selection row features, set the following settings: ##### Angular - +
+