feat(components): [select/select-v2] support debounce prop (#22471)

* feat(components): [select/select-v2] support debounce prop

* feat: update

* feat: update

* feat: update

* docs: update version number

* feat: update

* feat: update

* feat: update

* feat: update

* feat: update

* feat: update
This commit is contained in:
Rainbow
2025-10-20 10:46:24 +08:00
committed by GitHub
parent 8d32bc31d8
commit b917fafccc
8 changed files with 65 additions and 21 deletions

View File

@@ -275,6 +275,7 @@ select-v2/custom-width
| item-height | The height of the dropdown item | ^[number] | 34 |
| scrollbar-always-on | Controls whether the scrollbar is always displayed | ^[boolean] | false |
| remote | whether search data from server | ^[boolean] | false |
| debounce ^(2.11.6) | debounce delay during remote search, in milliseconds | ^[number] | 300 |
| remote-method | function that gets called when the input value changes. Its parameter is the current input value. To use this, `filterable` must be true | ^[Function]`(query: string) => void` | — |
| validate-event | whether to trigger form validation | ^[boolean] | true |
| offset ^(2.8.8) | offset of the dropdown | ^[number] | 12 |

View File

@@ -223,6 +223,7 @@ select/custom-label
| allow-create | whether creating new items is allowed. To use this, `filterable` must be true | ^[boolean] | false |
| filter-method | custom filter method, the first parameter is the current input value. To use this, `filterable` must be true | ^[Function]`(query: string) => void` | — |
| remote | whether options are loaded from server | ^[boolean] | false |
| debounce ^(2.11.6) | debounce delay during remote search, in milliseconds | ^[number] | 300 |
| remote-method | function that gets called when the input value changes. Its parameter is the current input value. To use this, `filterable` must be true | ^[Function]`(query: string) => void` | — |
| remote-show-suffix | in remote search method show suffix icon | ^[boolean] | false |
| loading | whether Select is loading data from server | ^[boolean] | false |

View File

@@ -23,6 +23,15 @@ vi.mock('lodash-unified', async () => {
}
})
vi.mock('@vueuse/core', async () => {
return {
...((await vi.importActual('@vueuse/core')) as Record<string, any>),
useDebounceFn: vi.fn((fn) => {
return fn
}),
}
})
const _mount = makeMountFunc({
components: {
'el-select': Select,
@@ -66,6 +75,7 @@ interface SelectProps {
allowCreate?: boolean
popperAppendToBody?: boolean
placeholder?: string
debounce?: number
[key: string]: any
}

View File

@@ -216,6 +216,13 @@ export const selectV2Props = buildProps({
* @description whether search data from server
*/
remote: Boolean,
/**
* @description debounce delay during remote search, in milliseconds
*/
debounce: {
type: Number,
default: 300,
},
/**
* @description size of component
*/

View File

@@ -7,19 +7,15 @@ import {
watch,
watchEffect,
} from 'vue'
import {
findLastIndex,
get,
isEqual,
debounce as lodashDebounce,
} from 'lodash-unified'
import { useResizeObserver } from '@vueuse/core'
import { findLastIndex, get, isEqual } from 'lodash-unified'
import { useDebounceFn, useResizeObserver } from '@vueuse/core'
import {
ValidateComponentsMap,
debugWarn,
escapeStringRegexp,
getEventCode,
isArray,
isEmpty,
isFunction,
isNumber,
isObject,
@@ -82,6 +78,7 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => {
// data refs
const popperSize = ref(-1)
const debouncing = ref(false)
// DOM & Component refs
const selectRef = ref<HTMLElement>()
@@ -179,7 +176,7 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => {
return ValidateComponentsMap[validateState.value]
})
const debounce = computed(() => (props.remote ? 300 : 0))
const debounce = computed(() => (props.remote ? props.debounce : 0))
const isRemoteSearchEmpty = computed(
() => props.remote && !states.inputValue && !hasOptions.value
@@ -390,7 +387,11 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => {
const dropdownMenuVisible = computed({
get() {
return expanded.value && (props.loading || !isRemoteSearchEmpty.value)
return (
expanded.value &&
(props.loading || !isRemoteSearchEmpty.value) &&
(!debouncing.value || !isEmpty(states.previousQuery))
)
},
set(val: boolean) {
expanded.value = val
@@ -445,7 +446,10 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => {
})
}
const debouncedOnInputChange = lodashDebounce(onInputChange, debounce.value)
const debouncedOnInputChange = useDebounceFn(() => {
onInputChange()
debouncing.value = false
}, debounce)
const handleQueryChange = (val: string) => {
if (states.previousQuery === val || isComposing.value) {
@@ -768,6 +772,7 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => {
const onInput = (event: Event) => {
states.inputValue = (event.target as HTMLInputElement).value
if (props.remote) {
debouncing.value = true
debouncedOnInputChange()
} else {
return onInputChange()

View File

@@ -22,6 +22,15 @@ vi.mock('lodash-unified', async () => {
}
})
vi.mock('@vueuse/core', async () => {
return {
...((await vi.importActual('@vueuse/core')) as Record<string, any>),
useDebounceFn: vi.fn((fn) => {
return fn
}),
}
})
interface SelectProps {
filterMethod?: any
remoteMethod?: any
@@ -38,6 +47,7 @@ interface SelectProps {
defaultFirstOption?: boolean
fitInputWidth?: boolean
size?: 'small' | 'default' | 'large'
debounce?: number
}
const _mount = (template: string, data: any = () => ({}), otherObj?) =>

View File

@@ -115,6 +115,13 @@ export const selectProps = buildProps({
* @description whether options are loaded from server
*/
remote: Boolean,
/**
* @description debounce delay during remote search, in milliseconds
*/
debounce: {
type: Number,
default: 300,
},
/**
* @description displayed text while loading data from server, default is 'Loading'
*/

View File

@@ -7,13 +7,8 @@ import {
watch,
watchEffect,
} from 'vue'
import {
findLastIndex,
get,
isEqual,
debounce as lodashDebounce,
} from 'lodash-unified'
import { useResizeObserver } from '@vueuse/core'
import { findLastIndex, get, isEqual } from 'lodash-unified'
import { useDebounceFn, useResizeObserver } from '@vueuse/core'
import {
ValidateComponentsMap,
debugWarn,
@@ -21,6 +16,7 @@ import {
getEventCode,
isArray,
isClient,
isEmpty,
isFunction,
isIOS,
isNumber,
@@ -97,6 +93,7 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => {
// the controller of the expanded popup
const expanded = ref(false)
const hoverOption = ref()
const debouncing = ref(false)
const { form, formItem } = useFormItem()
const { inputId } = useFormItemInputId(props, {
@@ -170,7 +167,7 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => {
(ValidateComponentsMap[validateState.value] as Component)
)
const debounce = computed(() => (props.remote ? 300 : 0))
const debounce = computed(() => (props.remote ? props.debounce : 0))
const isRemoteSearchEmpty = computed(
() => props.remote && !states.inputValue && states.options.size === 0
@@ -248,7 +245,11 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => {
const dropdownMenuVisible = computed({
get() {
return expanded.value && (props.loading || !isRemoteSearchEmpty.value)
return (
expanded.value &&
(props.loading || !isRemoteSearchEmpty.value) &&
(!debouncing.value || !isEmpty(states.previousQuery))
)
},
set(val: boolean) {
expanded.value = val
@@ -490,15 +491,17 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => {
const onInput = (event: Event) => {
states.inputValue = (event.target as HTMLInputElement).value
if (props.remote) {
debouncing.value = true
debouncedOnInputChange()
} else {
return onInputChange()
}
}
const debouncedOnInputChange = lodashDebounce(() => {
const debouncedOnInputChange = useDebounceFn(() => {
onInputChange()
}, debounce.value)
debouncing.value = false
}, debounce)
const emitChange = (val: OptionValue | OptionValue[]) => {
if (!isEqual(props.modelValue, val)) {