|
| 1 | +<template> |
| 2 | + <div class="dx-viewport demo-container"> |
| 3 | + <div class="dx-fieldset"> |
| 4 | + <div class="dx-field"> |
| 5 | + <div class="dx-field-label">DropDownBox with search and embedded DataGrid</div> |
| 6 | + <div class="dx-field-value"> |
| 7 | + <DxDropDownBox |
| 8 | + v-model:value="gridBoxValue" |
| 9 | + v-model:opened="gridBoxOpened" |
| 10 | + value-expr="OrderNumber" |
| 11 | + placeholder="Select a value..." |
| 12 | + value-change-event="" |
| 13 | + :data-source="dropDownBoxDataSource" |
| 14 | + :open-on-field-click="false" |
| 15 | + :display-expr="gridBoxDisplayExpr" |
| 16 | + :show-clear-button="true" |
| 17 | + :accept-custom-value="true" |
| 18 | + @value-changed="onValueChanged" |
| 19 | + @key-down="onKeyDown" |
| 20 | + @input="onInput" |
| 21 | + @opened="onOpened" |
| 22 | + @closed="onClosed" |
| 23 | + > |
| 24 | + <template #default> |
| 25 | + <DxDataGrid |
| 26 | + ref="dataGridRef" |
| 27 | + height="100%" |
| 28 | + width="100%" |
| 29 | + :column-width="100" |
| 30 | + v-model:selected-row-keys="gridBoxValue" |
| 31 | + v-model:focused-row-index="focusedRowIndex" |
| 32 | + v-model:focused-row-key="focusedRowKey" |
| 33 | + :data-source="dataSource" |
| 34 | + :remote-operations="true" |
| 35 | + :focused-row-enabled="true" |
| 36 | + :hover-state-enabled="true" |
| 37 | + :auto-navigate-to-focused-row="false" |
| 38 | + @key-down="dataGridKeyDown" |
| 39 | + @content-ready="dataGridContentReady" |
| 40 | + > |
| 41 | + <DxColumn |
| 42 | + data-field="OrderNumber" |
| 43 | + caption="ID" |
| 44 | + data-type="number" |
| 45 | + /> |
| 46 | + <DxColumn |
| 47 | + data-field="OrderDate" |
| 48 | + data-type="date" |
| 49 | + format="shortDate" |
| 50 | + /> |
| 51 | + <DxColumn |
| 52 | + data-field="StoreState" |
| 53 | + data-type="string" |
| 54 | + /> |
| 55 | + <DxColumn |
| 56 | + data-field="StoreCity" |
| 57 | + data-type="string" |
| 58 | + /> |
| 59 | + <DxColumn |
| 60 | + data-field="Employee" |
| 61 | + data-type="string" |
| 62 | + /> |
| 63 | + <DxColumn |
| 64 | + data-field="SaleAmount" |
| 65 | + data-type="number" |
| 66 | + > |
| 67 | + <DxFormat |
| 68 | + type="currency" |
| 69 | + :precision="2" |
| 70 | + /> |
| 71 | + </DxColumn> |
| 72 | + <DxPaging |
| 73 | + :enabled="true" |
| 74 | + :page-size="10" |
| 75 | + /> |
| 76 | + <DxSelection mode="single"/> |
| 77 | + <DxScrolling mode="virtual"/> |
| 78 | + </DxDataGrid> |
| 79 | + </template> |
| 80 | + </DxDropDownBox> |
| 81 | + </div> |
| 82 | + </div> |
| 83 | + </div> |
| 84 | + </div> |
| 85 | +</template> |
| 86 | + |
| 87 | +<script setup lang="ts"> |
| 88 | +import { ref } from 'vue'; |
| 89 | +import DataSource from 'devextreme/data/data_source'; |
| 90 | +import DxDropDownBox from 'devextreme-vue/drop-down-box'; |
| 91 | +import type { DxDataGridTypes } from 'devextreme-vue/data-grid'; |
| 92 | +import type { DxDropDownBoxTypes } from 'devextreme-vue/drop-down-box'; |
| 93 | +import { DxDataGrid, DxColumn, DxSelection, DxFormat, DxPaging, DxScrolling } from 'devextreme-vue/data-grid'; |
| 94 | +import notify from 'devextreme/ui/notify'; |
| 95 | +import type dxDropDownBox from 'devextreme/ui/drop_down_box'; |
| 96 | +// eslint-disable-next-line @typescript-eslint/ban-ts-comment |
| 97 | +// @ts-ignore third-party store no types |
| 98 | +import * as AspNetData from 'devextreme-aspnet-data-nojquery'; |
| 99 | +
|
| 100 | +interface OrderItem { |
| 101 | + OrderNumber: number; |
| 102 | + Employee: string; |
| 103 | + StoreState: string; |
| 104 | + StoreCity: string; |
| 105 | + OrderDate: string; |
| 106 | + SaleAmount: number; |
| 107 | +} |
| 108 | +
|
| 109 | +const dataGridRef = ref<InstanceType<typeof DxDataGrid> | null>(null); |
| 110 | +const gridBoxValue = ref<number[]>([35711]); |
| 111 | +const gridBoxOpened = ref<boolean>(false); |
| 112 | +const focusedRowIndex = ref<number>(0); |
| 113 | +const focusedRowKey = ref<number | null>(null); |
| 114 | +const searchTimer = ref<ReturnType<typeof setTimeout> | null>(null); |
| 115 | +const dataGridFirstLoadCompleted = ref<boolean>(false); |
| 116 | +const dataSource = ref<DataSource>( |
| 117 | + new DataSource({ |
| 118 | + store: makeAsyncDataSource(), |
| 119 | + searchExpr: ['StoreCity', 'StoreState', 'Employee'] |
| 120 | + } as any) |
| 121 | +); |
| 122 | +const dropDownBoxDataSource = ref<DataSource>( |
| 123 | + new DataSource({ |
| 124 | + store: makeAsyncDataSource() |
| 125 | + } as any) |
| 126 | +); |
| 127 | +
|
| 128 | +function makeAsyncDataSource(): unknown { |
| 129 | + return AspNetData.createStore({ |
| 130 | + key: 'OrderNumber', |
| 131 | + loadUrl: 'https://js.devexpress.com/Demos/WidgetsGalleryDataService/api/orders' |
| 132 | + }); |
| 133 | +} |
| 134 | +
|
| 135 | +function gridBoxDisplayExpr(item: OrderItem): string { |
| 136 | + return item ? `${item.Employee}: ${item.StoreState} - ${item.StoreCity} <${item.OrderNumber}>` : ''; |
| 137 | +} |
| 138 | +
|
| 139 | +function onValueChanged(): void { |
| 140 | + if (searchTimer.value) clearTimeout(searchTimer.value); |
| 141 | + gridBoxOpened.value = false; |
| 142 | +} |
| 143 | +
|
| 144 | +function isSearchIncomplete(dropDownBox: any): boolean { |
| 145 | + const displayValue = dropDownBox.option('displayValue') as string[] | undefined; |
| 146 | + const text = dropDownBox.option('text') as string | undefined; |
| 147 | + const textValue = text?.length ? text : undefined; |
| 148 | + const displayFirst = displayValue?.length ? displayValue[0] : undefined; |
| 149 | + return textValue !== displayFirst; |
| 150 | +} |
| 151 | +
|
| 152 | +function onInput(e: DxDropDownBoxTypes.InputEvent): void { |
| 153 | + if (searchTimer.value) clearTimeout(searchTimer.value); |
| 154 | + searchTimer.value = setTimeout(() => { |
| 155 | + const text = e.component.option('text') as string | undefined; |
| 156 | + dataSource.value.searchValue(text ?? null); |
| 157 | + if (gridBoxOpened.value && isSearchIncomplete(e.component)) { |
| 158 | + dataSource.value.load().then((items: OrderItem[]) => { |
| 159 | + if (items.length > 0 && dataGridRef.value?.instance) { |
| 160 | + focusedRowKey.value = items[0].OrderNumber; |
| 161 | + focusedRowIndex.value = 0; |
| 162 | + } |
| 163 | + }).catch((error: unknown) => notify(error, 'error', 1000)); |
| 164 | + } else { |
| 165 | + gridBoxOpened.value = true; |
| 166 | + } |
| 167 | + }, 500); |
| 168 | +} |
| 169 | +
|
| 170 | +function onOpened(e: DxDropDownBoxTypes.OpenedEvent): void { |
| 171 | + const ddbInstance = e.component as dxDropDownBox & { isKeyDown?: boolean }; |
| 172 | + if (ddbInstance.isKeyDown) { |
| 173 | + if (!dataGridRef.value?.instance) return; |
| 174 | + const inst = dataGridRef.value?.instance; |
| 175 | + const contentReadyHandler = ( |
| 176 | + args: DxDataGridTypes.ContentReadyEvent<OrderItem, number> |
| 177 | + ): void => { |
| 178 | + const gridInstance = args.component; |
| 179 | + gridInstance.focus(); |
| 180 | + gridInstance.off('contentReady', contentReadyHandler as any); |
| 181 | + }; |
| 182 | + if (!dataGridFirstLoadCompleted.value) { |
| 183 | + inst.on('contentReady', contentReadyHandler as any); |
| 184 | + } else { |
| 185 | + const optionChangedHandler = ( |
| 186 | + args: DxDataGridTypes.OptionChangedEvent<OrderItem, number> |
| 187 | + ): void => { |
| 188 | + const gridInstance = args.component; |
| 189 | + if (args.name === 'focusedRowKey' || args.name === 'focusedColumnIndex') { |
| 190 | + gridInstance.off('optionChanged', optionChangedHandler as any); |
| 191 | + gridInstance.focus(); |
| 192 | + } |
| 193 | + }; |
| 194 | + inst.on('optionChanged', optionChangedHandler as any); |
| 195 | + focusedRowIndex.value = 0; |
| 196 | + } |
| 197 | + ddbInstance.isKeyDown = false; |
| 198 | + } else if (dataGridFirstLoadCompleted.value && isSearchIncomplete(ddbInstance)) { |
| 199 | + void dataSource.value.load().then((items: OrderItem[]) => { |
| 200 | + if (items.length > 0) focusedRowKey.value = items[0].OrderNumber; |
| 201 | + ddbInstance.focus(); |
| 202 | + }); |
| 203 | + } |
| 204 | +} |
| 205 | +
|
| 206 | +function onClosed(e: DxDropDownBoxTypes.ClosedEvent): void { |
| 207 | + const ddbInstance = e.component; |
| 208 | + const searchValue = dataSource.value.searchValue(); |
| 209 | + if (isSearchIncomplete(ddbInstance)) { |
| 210 | + gridBoxValue.value = []; |
| 211 | + } |
| 212 | + if (searchValue) { |
| 213 | + dataSource.value.searchValue(null); |
| 214 | + } |
| 215 | +} |
| 216 | +
|
| 217 | +function onKeyDown(e: DxDropDownBoxTypes.KeyDownEvent): void { |
| 218 | + if (e.event?.keyCode !== 40) return; |
| 219 | + const ddbInstance = e.component as dxDropDownBox & { isKeyDown?: boolean }; |
| 220 | + if (!gridBoxOpened.value) { |
| 221 | + ddbInstance.isKeyDown = true; |
| 222 | + gridBoxOpened.value = true; |
| 223 | + } else if (dataGridRef.value?.instance) { |
| 224 | + dataGridRef.value.instance.focus(); |
| 225 | + } |
| 226 | +} |
| 227 | +
|
| 228 | +function dataGridContentReady(): void { |
| 229 | + if (!dataGridFirstLoadCompleted.value) { |
| 230 | + dataGridFirstLoadCompleted.value = true; |
| 231 | + } |
| 232 | +} |
| 233 | +
|
| 234 | +function dataGridKeyDown(e: DxDataGridTypes.KeyDownEvent): void { |
| 235 | + if (e.event?.keyCode === 13 && focusedRowKey.value != null) { |
| 236 | + gridBoxValue.value = [focusedRowKey.value]; |
| 237 | + gridBoxOpened.value = false; |
| 238 | + } |
| 239 | +} |
| 240 | +</script> |
| 241 | + |
| 242 | +<style scoped> |
| 243 | +.dx-fieldset { margin: 16px; } |
| 244 | +.dx-field-label { font-weight: 600; margin-bottom: 8px; } |
| 245 | +</style> |
0 commit comments