feat(components): select-v2 support filter-method & remote-search (#3092)

This commit is contained in:
msidolphin 2021-08-29 09:46:38 +08:00 committed by GitHub
parent 8f67fb7645
commit a15f5f293f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 655 additions and 183 deletions

View File

@ -24,7 +24,7 @@ const clickClearButton = async wrapper => {
const select = wrapper.findComponent(Select)
const selectVm = select.vm as any
selectVm.states.comboBoxHovering = true
await nextTick
await nextTick()
const clearBtn = wrapper.find(`.${selectVm.clearIcon}`)
expect(clearBtn.exists()).toBeTruthy()
await clearBtn.trigger('click')
@ -38,6 +38,7 @@ interface SelectProps {
clearable?: boolean
multiple?: boolean
filterable?: boolean
remote?: boolean
multipleLimit?: number
allowCreate?: boolean
popperAppendToBody?: boolean
@ -51,6 +52,8 @@ interface SelectEvents {
onRemoveTag?: (tag?: string) => void
onFocus?: (event?: FocusEvent) => void
onBlur?: (event?) => void
filterMethod?: (query: string) => void | undefined
remoteMethod?: (query: string) => void | undefined
[key: string]: (...args) => any
}
@ -76,6 +79,9 @@ const createSelect = (options: {
:popper-append-to-body="popperAppendToBody"
:placeholder="placeholder"
:allow-create="allowCreate"
:remote="remote"
${options.methods && options.methods.filterMethod ? `:filter-method="filterMethod"` : ''}
${options.methods && options.methods.remoteMethod ? `:remote-method="remoteMethod"` : ''}
@change="onChange"
@visible-change="onVisibleChange"
@remove-tah="onRemoveTag"
@ -95,6 +101,7 @@ const createSelect = (options: {
disabled: false,
clearable: false,
multiple: false,
remote: false,
filterable: false,
multipleLimit: 0,
popperAppendToBody: true,
@ -115,7 +122,7 @@ const createSelect = (options: {
function getOptions(): HTMLElement[] {
return Array.from(document.querySelectorAll<HTMLElement>(
`body > div:last-child .${OPTION_ITEM_CLASS_NAME}`,
`.${OPTION_ITEM_CLASS_NAME}`,
))
}
@ -133,7 +140,7 @@ describe('Select', () => {
it('create', async () => {
const wrapper = createSelect()
await nextTick
await nextTick()
expect(wrapper.classes()).toContain(CLASS_NAME)
expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toContain(DEFAULT_PLACEHOLDER)
const select = wrapper.findComponent(Select)
@ -143,7 +150,7 @@ describe('Select', () => {
it('options rendered correctly', async() => {
const wrapper = createSelect()
await nextTick
await nextTick()
const vm = wrapper.vm as any
const options = document.getElementsByClassName(OPTION_ITEM_CLASS_NAME)
const result = [].every.call(options, (option, index) => {
@ -159,7 +166,7 @@ describe('Select', () => {
popperClass: 'custom-dropdown',
}),
})
await nextTick
await nextTick()
expect(document.querySelector('.el-popper').classList).toContain('custom-dropdown')
})
@ -184,7 +191,7 @@ describe('Select', () => {
}),
})
const vm = wrapper.vm as any
await nextTick
await nextTick()
expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe(vm.options[1].label)
})
@ -194,23 +201,23 @@ describe('Select', () => {
const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER)
vm.value = vm.options[2].value
await nextTick
await nextTick()
expect(placeholder.text()).toBe(vm.options[2].label)
vm.value = null
await nextTick
await nextTick()
expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER)
})
it('sync set value and options', async () => {
const wrapper = createSelect()
await nextTick
await nextTick()
const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
const vm = wrapper.vm as any
vm.value = vm.options[1].value
await nextTick
await nextTick()
expect(placeholder.text()).toBe(vm.options[1].label)
vm.options[1].label = 'option bb aa'
await nextTick
await nextTick()
expect(placeholder.text()).toBe('option bb aa')
})
@ -227,18 +234,18 @@ describe('Select', () => {
},
},
})
await nextTick
await nextTick()
const options = getOptions()
const vm = wrapper.vm as any
const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
expect(vm.value).toBe('')
expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER)
options[2].click()
await nextTick
await nextTick()
expect(vm.value).toBe(vm.options[2].value)
expect(placeholder.text()).toBe(vm.options[2].label)
options[4].click()
await nextTick
await nextTick()
expect(vm.value).toBe(vm.options[4].value)
expect(placeholder.text()).toBe(vm.options[4].label)
expect(vm.count).toBe(2)
@ -268,22 +275,22 @@ describe('Select', () => {
}
},
})
await nextTick
await nextTick()
const vm = wrapper.vm as any
const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
const option = document.querySelector<HTMLElement>(`.el-select-dropdown__option-item.is-disabled`)
expect(option.textContent).toBe(vm.options[1].label)
option.click()
await nextTick
await nextTick()
expect(vm.value).toBe('')
expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER)
vm.options[2].disabled = true
await nextTick
await nextTick()
const options = document.querySelectorAll<HTMLElement>(`.el-select-dropdown__option-item.is-disabled`)
expect(options.length).toBe(2)
expect(options.item(1).textContent).toBe(vm.options[2].label)
options.item(1).click()
await nextTick
await nextTick()
expect(vm.value).toBe('')
expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER)
})
@ -296,7 +303,7 @@ describe('Select', () => {
}
},
})
await nextTick
await nextTick()
expect(wrapper.find(`.${WRAPPER_CLASS_NAME}`).classes()).toContain('is-disabled')
})
@ -313,7 +320,7 @@ describe('Select', () => {
},
},
})
await nextTick
await nextTick()
const vm = wrapper.vm as any
await wrapper.trigger('click')
expect(vm.visible).toBeTruthy()
@ -325,7 +332,7 @@ describe('Select', () => {
})
const vm = wrapper.vm as any
vm.value = vm.options[1].value
await nextTick
await nextTick()
await clickClearButton(wrapper)
expect(vm.value).toBe('')
const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
@ -343,15 +350,15 @@ describe('Select', () => {
}
},
})
await nextTick
await nextTick()
const vm = wrapper.vm as any
const options = getOptions()
options[1].click()
await nextTick
await nextTick()
expect(vm.value.length).toBe(1)
expect(vm.value[0]).toBe(vm.options[1].value)
options[3].click()
await nextTick
await nextTick()
expect(vm.value.length).toBe(2)
expect(vm.value[1]).toBe(vm.options[3].value)
const tagIcon = wrapper.find('.el-tag__close')
@ -373,7 +380,7 @@ describe('Select', () => {
},
},
})
await nextTick
await nextTick()
const vm = wrapper.vm as any
const options = getOptions()
options[0].click()
@ -400,16 +407,16 @@ describe('Select', () => {
}
},
})
await nextTick
await nextTick()
const vm = wrapper.vm as any
const options = getOptions()
options[1].click()
await nextTick
await nextTick()
options[2].click()
await nextTick
await nextTick()
expect(vm.value.length).toBe(2)
options[3].click()
await nextTick
await nextTick()
expect(vm.value.length).toBe(2)
})
})
@ -431,12 +438,12 @@ describe('Select', () => {
const selectVm = select.vm as any
// Simulate focus state to trigger menu multiple times
selectVm.toggleMenu()
await nextTick
await nextTick()
selectVm.toggleMenu()
await nextTick
await nextTick()
// Simulate click the outside
selectVm.handleClickOutside()
await nextTick
await nextTick()
expect(onFocus).toHaveBeenCalledTimes(1)
expect(onBlur).toHaveBeenCalled()
})
@ -463,20 +470,20 @@ describe('Select', () => {
const selectVm = select.vm as any
// Simulate focus state to trigger menu multiple times
selectVm.toggleMenu()
await nextTick
await nextTick()
selectVm.toggleMenu()
await nextTick
await nextTick()
// Select multiple items in multiple mode without triggering focus
const options = getOptions()
options[1].click()
await nextTick
await nextTick()
options[2].click()
await nextTick
await nextTick()
expect(onFocus).toHaveBeenCalledTimes(1)
// Simulate click the outside
selectVm.handleClickOutside()
await nextTick
await nextTick
await nextTick()
await nextTick()
expect(onBlur).toHaveBeenCalled()
})
@ -487,14 +494,14 @@ describe('Select', () => {
onChange: handleChanged,
},
})
await nextTick
await nextTick()
const vm = wrapper.vm as any
vm.value = 'option_2'
await nextTick
await nextTick()
expect(handleChanged).toHaveBeenCalledTimes(0)
const options = getOptions()
options[4].click()
await nextTick
await nextTick()
expect(handleChanged).toHaveBeenCalled()
})
})
@ -505,6 +512,7 @@ describe('Select', () => {
const wrapper = createSelect({
data: () => {
return {
popperAppendToBody: false,
allowCreate: true,
filterable: true,
clearable: true,
@ -525,23 +533,25 @@ describe('Select', () => {
}
},
})
await nextTick
const vm = wrapper.vm as any
const input = wrapper.find('input')
await wrapper.trigger('click')
// create a new option
await input.trigger('compositionupdate', {
data: '1111',
})
const options = getOptions()
await nextTick()
const select = wrapper.findComponent(Select)
const selectVm = select.vm as any
selectVm.expanded = true
await nextTick()
const vm = wrapper.vm as any
const input = wrapper.find('input')
// create a new option
input.element.value = '1111'
await input.trigger('input')
await nextTick()
expect(selectVm.filteredOptions.length).toBe(1)
// selected the new option
await options[0].click()
selectVm.onSelect(selectVm.filteredOptions[0])
expect(vm.value).toBe('1111')
// closed the menu
await wrapper.trigger('click')
selectVm.expanded = false
await nextTick()
selectVm.expanded = true
await nextTick()
expect(selectVm.filteredOptions.length).toBe(4)
selectVm.handleClear()
expect(selectVm.filteredOptions.length).toBe(3)
@ -572,24 +582,24 @@ describe('Select', () => {
}
},
})
await nextTick
await nextTick()
const vm = wrapper.vm as any
await wrapper.trigger('click')
await wrapper.find('input').trigger('compositionupdate', {
data: '1111',
})
const options = getOptions()
const input = wrapper.find('input')
input.element.value = '1111'
await input.trigger('input')
await nextTick()
const select = wrapper.findComponent(Select)
const selectVm = select.vm as any
expect(selectVm.filteredOptions.length).toBe(1)
// selected the new option
await options[0].click()
selectVm.onSelect(selectVm.filteredOptions[0])
// closed the menu
await wrapper.trigger('click')
await wrapper.find('input').trigger('compositionupdate', {
data: '2222',
})
await getOptions()[0].click()
input.element.value = '2222'
await input.trigger('input')
await nextTick()
selectVm.onSelect(selectVm.filteredOptions[0])
expect(JSON.stringify(vm.value)).toBe(JSON.stringify(['1111', '2222']))
await wrapper.trigger('click')
expect(selectVm.filteredOptions.length).toBe(5)
@ -618,7 +628,7 @@ describe('Select', () => {
empty: '<div class="empty-slot">EmptySlot</div>',
},
})
await nextTick
await nextTick()
expect(wrapper.find('.empty-slot').exists()).toBeTruthy()
})
@ -645,11 +655,11 @@ describe('Select', () => {
}
},
})
await nextTick
await nextTick()
const vm = wrapper.vm as any
const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
vm.value = '2'
await nextTick
await nextTick()
const select = wrapper.findComponent(Select)
const selectVm = select.vm as any
selectVm.toggleMenu()
@ -666,12 +676,12 @@ describe('Select', () => {
}
},
})
await nextTick
await nextTick()
const vm = wrapper.vm as any
const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER)
vm.value = undefined
await nextTick
await nextTick()
expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER)
})
@ -683,7 +693,7 @@ describe('Select', () => {
}
},
})
await nextTick
await nextTick()
const vm = wrapper.vm as any
const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
expect(placeholder.text()).toBe(vm.value)
@ -707,7 +717,7 @@ describe('Select', () => {
`,
},
})
await nextTick
await nextTick()
expect(wrapper.findAll('.custom-renderer').length).toBeGreaterThan(0)
})
@ -719,24 +729,24 @@ describe('Select', () => {
options: [
{
value: 1,
lable: 'option 1',
label: 'option 1',
disabled: true,
},
{
value: 2,
lable: 'option 2',
label: 'option 2',
disabled: true,
},
{
value: 3,
lable: 'option 3',
label: 'option 3',
},
],
value: [2, 3],
}
},
})
await nextTick
await nextTick()
expect(wrapper.findAll('.el-tag').length).toBe(2)
const tagCloseIcons = wrapper.findAll('.el-tag__close')
expect(tagCloseIcons.length).toBe(1)
@ -754,11 +764,11 @@ describe('Select', () => {
}
},
})
await nextTick
await nextTick()
expect(wrapper.findAll('.el-tag').length).toBe(3)
const vm = wrapper.vm as any
vm.value.splice(0, 1)
await nextTick
await nextTick()
expect(wrapper.findAll('.el-tag').length).toBe(2)
})
@ -773,7 +783,7 @@ describe('Select', () => {
}
},
})
await nextTick
await nextTick()
expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).exists()).toBeFalsy()
// When all tags are removed, the placeholder should be displayed
const tagCloseIcon = wrapper.find('.el-tag__close')
@ -782,14 +792,109 @@ describe('Select', () => {
// The placeholder should disappear after it is selected again
const options = getOptions()
options[0].click()
await nextTick
await nextTick()
expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).exists()).toBeFalsy()
// Simulate keyboard events
const selectInput = wrapper.find('input')
selectInput.trigger('keydown', {
key: EVENT_CODE.backspace,
})
await nextTick
await nextTick()
expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe(DEFAULT_PLACEHOLDER)
})
describe('filter method', () => {
async function testFilterMethod({ multiple = false }) {
const filterMethod = jest.fn()
const wrapper = createSelect({
data() {
return {
filterable: true,
multiple,
}
},
methods: {
filterMethod,
},
})
const input = wrapper.find('input')
input.element.value = 'query'
await input.trigger('input')
expect(filterMethod).toHaveBeenCalled()
}
it('should call filter method', async () => {
await testFilterMethod({ multiple: false })
})
it('should call filter method in multiple mode', async () => {
await testFilterMethod({ multiple: true })
})
it('should re-render', async () => {
const wrapper = createSelect({
data() {
return {
multiple: true,
filterable: true,
}
},
methods: {
filterMethod () {
this.options = [
{
value: 1,
label: 'option 1',
},
{
value: 2,
label: 'option 2',
},
{
value: 3,
label: 'option 3',
},
]
},
},
})
const input = wrapper.find('input')
input.element.value = 'query'
await input.trigger('input')
await nextTick()
input.element.value = ''
await input.trigger('input')
await nextTick()
const options = getOptions()
expect(options.length).toBe(3)
})
})
describe('remote search', () => {
async function testRemoteSearch({ multiple = false }) {
const remoteMethod = jest.fn()
const wrapper = createSelect({
data() {
return {
filterable: true,
remote: true,
multiple,
}
},
methods: {
remoteMethod,
},
})
const input = wrapper.find('input')
input.element.value = 'query'
await input.trigger('input')
expect(remoteMethod).toHaveBeenCalled()
}
it('should call remote method', async () => {
await testRemoteSearch({ multiple: false })
})
it('should call remote method in multiple mode', async () => {
await testRemoteSearch({ multiple: true })
})
})
})

View File

@ -6,7 +6,7 @@
'el-select-dropdown__option-item': true,
'is-selected': selected,
'is-disabled': disabled,
'is-craeted': created,
'is-created': created,
'hover': hovering
}"
@mouseenter="hoverItem"

View File

@ -10,7 +10,7 @@
>
<el-popper
ref="popper"
v-model:visible="expanded"
v-model:visible="dropdownMenuVisible"
:append-to-body="popperAppendToBody"
:popper-class="`el-select-v2__popper ${popperClass}`"
:gpu-acceleration="false"
@ -109,11 +109,11 @@
:name="name"
:unselectable="expanded ? 'on' : undefined"
@update:modelValue="onUpdateInputValue"
@click.stop.prevent="handleInputBoxClick"
@focus="handleFocus"
@input="onInput"
@compositionupdate="onCompositionUpdate"
@compositionend="onInput"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@keydown.esc.stop.prevent="handleEsc"
@keydown.delete.stop="handleDel"
>
@ -149,9 +149,9 @@
spellcheck="false"
type="text"
:unselectable="expanded ? 'on' : undefined"
@click.stop.prevent="handleInputBoxClick"
@compositionend="onInput"
@compositionupdate="onCompositionUpdate"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@focus="handleFocus"
@input="onInput"
@keydown.esc.stop.prevent="handleEsc"
@ -203,7 +203,7 @@
</template>
<template #empty>
<slot name="empty">
<p class="el-select-v2__empty">{{ emptyText }}</p>
<p class="el-select-v2__empty">{{ emptyText ? emptyText : '' }}</p>
</slot>
</template>
</el-select-menu>

View File

@ -0,0 +1,32 @@
import { ref } from 'vue'
import { isKorean } from '@element-plus/utils/isDef'
import { isFunction } from '@vue/shared'
export function useInput(handleInput: (event: InputEvent) => void) {
const isComposing = ref(false)
const handleCompositionStart = () => {
isComposing.value = true
}
const handleCompositionUpdate = event => {
const text = event.target.value
const lastCharacter = text[text.length - 1] || ''
isComposing.value = !isKorean(lastCharacter)
}
const handleCompositionEnd = event => {
if (isComposing.value) {
isComposing.value = false
if (isFunction(handleInput)) {
handleInput(event)
}
}
}
return {
handleCompositionStart,
handleCompositionUpdate,
handleCompositionEnd,
}
}

View File

@ -33,9 +33,10 @@ import { flattenOptions } from './util'
import type { ExtractPropTypes, CSSProperties } from 'vue'
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
import type { OptionType, Option } from './select.types'
import { useInput } from './useInput'
const DEFAULT_INPUT_PLACEHOLDER = ''
const MINIMUM_INPUT_WIDTH = 4
const MINIMUM_INPUT_WIDTH = 11
const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
@ -64,6 +65,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
inputWidth: 240,
initialInputHeight: 0,
previousQuery: null,
previousValue: '',
query: '',
selectedLabel: '',
softFocus: false,
@ -116,8 +118,8 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
if (props.loading) {
return props.loadingText || t('el.select.loading')
} else {
if (props.remote && states.query === '' && options.length === 0) return false
if (props.filterable && states.query && options.length > 0) {
if (props.remote && states.inputValue === '' && options.length === 0) return false
if (props.filterable && states.inputValue && options.length > 0) {
return props.noMatchText || t('el.select.noMatch')
}
if (options.length === 0) {
@ -135,7 +137,9 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
const containsQueryString = query ? o.label.includes(query) : true
return containsQueryString
}
if (props.loading) {
return []
}
return flattenOptions((props.options as OptionType[]).concat(states.createdOptions).map(v => {
if (isArray(v.options)) {
const filtered = v.options.filter(isValidOption)
@ -146,7 +150,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
}
}
} else {
if (isValidOption(v as Option)) {
if (props.remote || isValidOption(v as Option)) {
return v
}
}
@ -161,13 +165,11 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
const calculatePopperSize = () => {
popperSize.value = selectRef.value?.getBoundingClientRect?.()?.width || 200
}
// const readonly = computed(() => !props.filterable || props.multiple || (!isIE() && !isEdge() && !expanded.value))
const inputWrapperStyle = computed(() => {
return {
width: `${
// 7 represents the margin-left value
states.calculatedWidth === 0
? MINIMUM_INPUT_WIDTH
: Math.ceil(states.calculatedWidth) + MINIMUM_INPUT_WIDTH
@ -209,8 +211,11 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
return -1
})
const dropdownMenuVisible = computed(() => expanded.value && emptyText.value !== false)
// hooks
const { createNewOption, removeNewOption, selectNewOption, clearAllNewOption } = useAllowCreate(props, states)
const { handleCompositionStart, handleCompositionUpdate, handleCompositionEnd } = useInput(e => onInput(e))
// methods
const focusAndUpdatePopup = () => {
@ -221,78 +226,35 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
const toggleMenu = () => {
if (props.automaticDropdown) return
if (!selectDisabled.value) {
// if (states.menuVisibleOnFocus) {
// states.menuVisibleOnFocus = false
// } else {
// if (expanded.value) {
// expanded.value = false
// }
if (states.isComposing) states.softFocus = true
expanded.value = !expanded.value
inputRef.value?.focus?.()
// }
}
}
const handleQueryChange = (val: string) => {
if (states.previousQuery === val || states.isOnComposition) return
if (
states.previousQuery === null &&
(isFunction(props.filterMethod) || isFunction(props.remoteMethod))
) {
states.previousQuery = val
return
}
states.previousQuery = val
nextTick(() => {
if (expanded.value) popper.value?.update?.()
})
states.hoveringIndex = -1
if (props.multiple && props.filterable) {
nextTick(() => {
const length = inputRef.value.value.length * 15 + 20
states.inputLength = props.collapseTags ? Math.min(50, length) : length
resetInputHeight()
})
}
if (props.remote && isFunction(props.remoteMethod)) {
states.hoveringIndex = -1
props.remoteMethod(val)
} else if (isFunction(props.filterMethod)) {
props.filterMethod(val)
// states.selectEmitter.emit('elOptionGroupQueryChange')
} else {
// states.selectEmitter.emit('elOptionQueryChange', val)
// states.selectEmitter.emit('elOptionGroupQueryChange')
}
if (props.defaultFirstOption && (props.filterable || props.remote)) {
// checkDefaultFirstOption()
}
}
// const handleComposition = event => {
// const text = event.target.value
// if (event.type === 'compositionend') {
// states.isOnComposition = false
// nextTick(() => handleQueryChange(text))
// } else {
// const lastCharacter = text[text.length - 1] || ''
// states.isOnComposition = !isKorean(lastCharacter)
// }
// }
const onInputChange = () => {
if (props.filterable && states.inputValue !== states.selectedLabel) {
states.query = states.selectedLabel
handleQueryChange(states.query)
}
handleQueryChange(states.inputValue)
return nextTick(() => {
createNewOption(states.inputValue)
})
}
const debouncedOnInputChange = lodashDebounce(onInputChange, debounce.value)
const debouncedQueryChange = lodashDebounce(e => {
handleQueryChange(e.target.value)
}, debounce.value)
const handleQueryChange = (val: string) => {
if (states.previousQuery === val) {
return
}
states.previousQuery = val
if (props.filterable && isFunction(props.filterMethod)) {
props.filterMethod(val)
} else if (props.filterable && props.remote && isFunction(props.remoteMethod)) {
props.remoteMethod(val)
}
}
const emitChange = (val: any | any[]) => {
if (!isEqual(props.modelValue, val)) {
@ -303,9 +265,9 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
const update = (val: any) => {
emit(UPDATE_MODEL_EVENT, val)
emitChange(val)
states.previousValue = val.toString()
}
// TODO 提取
const getValueIndex = (arr = [], value: unknown) => {
if (!isObject(value)) return arr.indexOf(value)
@ -425,12 +387,6 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
event.stopPropagation()
}
const handleInputBoxClick = () => {
if (states.displayInputValue.length === 0 && expanded.value) {
expanded.value = false
}
}
const handleFocus = (event: FocusEvent) => {
const focused = states.isComposing
states.isComposing = true
@ -565,7 +521,9 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
}
}
const onInput = () => {
const onInput = event => {
const value = event.target.value
onUpdateInputValue(value)
if (states.displayInputValue.length > 0 && !expanded.value) {
expanded.value = true
}
@ -574,13 +532,11 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
if (props.multiple) {
resetInputHeight()
}
debouncedOnInputChange()
createNewOption(states.displayInputValue)
}
const onCompositionUpdate = (e: CompositionEvent) => {
onUpdateInputValue(states.displayInputValue += e.data)
onInput()
if (props.remote) {
debouncedOnInputChange()
} else {
return onInputChange()
}
}
const handleClickOutside = () => {
@ -590,7 +546,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
const handleMenuEnter = () => {
states.inputValue = states.displayInputValue
nextTick(() => {
return nextTick(() => {
if (~indexRef.value) {
scrollToItem(indexRef.value)
}
@ -642,8 +598,20 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
}
})
watch([() => props.modelValue, () => props.options], () => {
initStates()
watch(() => props.modelValue, val => {
if (!val || val.toString() !== states.previousValue) {
initStates()
}
}, {
deep: true,
})
watch(() => props.options, () => {
const input = inputRef.value
// filter or remote-search scenarios are not initialized
if (!input || (input && document.activeElement !== input)) {
initStates()
}
}, {
deep: true,
})
@ -674,6 +642,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
iconClass,
inputWrapperStyle,
popperSize,
dropdownMenuVisible,
// readonly,
shouldShowPlaceholder,
selectDisabled,
@ -694,7 +663,6 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
// methods exports
debouncedOnInputChange,
debouncedQueryChange,
deleteTag,
getLabel,
getValueKey,
@ -704,16 +672,17 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
handleDel,
handleEsc,
handleFocus,
handleInputBoxClick,
handleMenuEnter,
toggleMenu,
scrollTo: scrollToItem,
onCompositionUpdate,
onInput,
onKeyboardNavigate,
onKeyboardSelect,
onSelect,
onUpdateInputValue,
handleCompositionStart,
handleCompositionEnd,
handleCompositionUpdate,
}
}

View File

@ -226,7 +226,7 @@ $--input-inline-start: 15px !default;
right: 5px;
height: 40px;
top: 50%;
margin-top: -20px;
transform: translateY(-50%);
}
@include e(caret) {

View File

@ -365,7 +365,78 @@ Create and select new items that are not included in select options
### Remote search
WIP 👷‍♀️
Enter keywords and search data from server.
:::demo Set the value of `filterable` and `remote` with `true` to enable remote search, and you should pass the `remote-method`. `remote-method` is a `Function` that gets called when the input value changes, and its parameter is the current input value.
```html
<template>
<el-select-v2
v-model="value"
style="width:200px"
multiple
size="medium"
filterable
remote
:remote-method="remoteMethod"
clearable
:options="options"
:loading="loading"
placeholder="Please enter a keyword"
/>
</template>
<script>
export default {
created() {
this.list = this.states.map(item => {
return { value: `value:${item}`, label: `label:${item}` }
})
},
methods: {
remoteMethod(query) {
if (query !== '') {
this.loading = true
setTimeout(() => {
this.loading = false
this.options = this.list.filter(item => {
return item.label.toLowerCase()
.indexOf(query.toLowerCase()) > -1
})
}, 200)
} else {
this.options = []
}
},
},
data() {
return {
list: [],
loading: false,
states: ['Alabama', 'Alaska', 'Arizona',
'Arkansas', 'California', 'Colorado',
'Connecticut', 'Delaware', 'Florida',
'Georgia', 'Hawaii', 'Idaho', 'Illinois',
'Indiana', 'Iowa', 'Kansas', 'Kentucky',
'Louisiana', 'Maine', 'Maryland',
'Massachusetts', 'Michigan', 'Minnesota',
'Mississippi', 'Missouri', 'Montana',
'Nebraska', 'Nevada', 'New Hampshire',
'New Jersey', 'New Mexico', 'New York',
'North Carolina', 'North Dakota', 'Ohio',
'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina',
'South Dakota', 'Tennessee', 'Texas',
'Utah', 'Vermont', 'Virginia',
'Washington', 'West Virginia', 'Wisconsin',
'Wyoming'],
options: [],
value: [],
}
},
}
</script>
```
:::
### Keyboard navigation

View File

@ -364,9 +364,80 @@ Crear y seleccionar nuevos items que no están incluidas en las opciones de sele
```
:::
### Remote search
### Búsqueda remota
WIP 👷‍♀️
Introduzca palabras y datos para buscar desde el servidor.
:::demo Configure el valor de `filterable` y `remote` con `true` para habilitar la búsqueda remota, y debería pasar el método `remote-method`. `remote-method` es una función que se llama cuando el valor del input cambia, y su parámetro es el valor del input actual.
```html
<template>
<el-select-v2
v-model="value"
style="width:200px"
multiple
size="medium"
filterable
remote
:remote-method="remoteMethod"
clearable
:options="options"
:loading="loading"
placeholder="Please enter a keyword"
/>
</template>
<script>
export default {
created() {
this.list = this.states.map(item => {
return { value: `value:${item}`, label: `label:${item}` }
})
},
methods: {
remoteMethod(query) {
if (query !== '') {
this.loading = true
setTimeout(() => {
this.loading = false
this.options = this.list.filter(item => {
return item.label.toLowerCase()
.indexOf(query.toLowerCase()) > -1
})
}, 200)
} else {
this.options = []
}
},
},
data() {
return {
list: [],
loading: false,
states: ['Alabama', 'Alaska', 'Arizona',
'Arkansas', 'California', 'Colorado',
'Connecticut', 'Delaware', 'Florida',
'Georgia', 'Hawaii', 'Idaho', 'Illinois',
'Indiana', 'Iowa', 'Kansas', 'Kentucky',
'Louisiana', 'Maine', 'Maryland',
'Massachusetts', 'Michigan', 'Minnesota',
'Mississippi', 'Missouri', 'Montana',
'Nebraska', 'Nevada', 'New Hampshire',
'New Jersey', 'New Mexico', 'New York',
'North Carolina', 'North Dakota', 'Ohio',
'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina',
'South Dakota', 'Tennessee', 'Texas',
'Utah', 'Vermont', 'Virginia',
'Washington', 'West Virginia', 'Wisconsin',
'Wyoming'],
options: [],
value: [],
}
},
}
</script>
```
:::
### Keyboard navigation
@ -391,6 +462,9 @@ Some APIs are still undergoing (comparing to the non-virtualized select), becaus
| autocomplete | select input 的 autocomplete 属性 | string | — | off |
| placeholder | the autocomplete attribute of select input | string | — | Please select |
| filterable | is filterable | boolean | — | false |
| filter-method | método de filtrado personalizado | function | — | — |
| remote | si las opciones se traerán desde el servidor | boolean | — | false |
| remote-method | método de búsqueda remota personalizada | function | — | — |
| allow-create | si esta permitido crear nuevos items. Para usar esto, `filterable` debe ser `true`. | boolean | — | false |
| no-data-text | displayed text when there is no options, you can also use slot empty | string | — | No Data |
| popper-class | custom class name for Select's dropdown | string | — | — |

View File

@ -365,9 +365,80 @@ Vous pouvez entrer des choix dans le champ de sélection qui ne sont pas incluse
```
:::
### Remote search
### Recherche à distance
WIP 👷‍♀️
Vous pouvez aller chercher les options sur le serveur de manière dynamique.
:::demo Ajoutez `filterable` et `remote` pour activer la recherche distante, ainsi que `remote-method`. Cette dernière est une `Function` qui est appelée lorsque la valeur change, avec pour paramètre la valeur courante.
```html
<template>
<el-select-v2
v-model="value"
style="width:200px"
multiple
size="medium"
filterable
remote
:remote-method="remoteMethod"
clearable
:options="options"
:loading="loading"
placeholder="Entrez un mot-clé"
/>
</template>
<script>
export default {
created() {
this.list = this.states.map(item => {
return { value: `value:${item}`, label: `label:${item}` }
})
},
methods: {
remoteMethod(query) {
if (query !== '') {
this.loading = true
setTimeout(() => {
this.loading = false
this.options = this.list.filter(item => {
return item.label.toLowerCase()
.indexOf(query.toLowerCase()) > -1
})
}, 200)
} else {
this.options = []
}
},
},
data() {
return {
list: [],
loading: false,
states: ['Alabama', 'Alaska', 'Arizona',
'Arkansas', 'California', 'Colorado',
'Connecticut', 'Delaware', 'Florida',
'Georgia', 'Hawaii', 'Idaho', 'Illinois',
'Indiana', 'Iowa', 'Kansas', 'Kentucky',
'Louisiana', 'Maine', 'Maryland',
'Massachusetts', 'Michigan', 'Minnesota',
'Mississippi', 'Missouri', 'Montana',
'Nebraska', 'Nevada', 'New Hampshire',
'New Jersey', 'New Mexico', 'New York',
'North Carolina', 'North Dakota', 'Ohio',
'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina',
'South Dakota', 'Tennessee', 'Texas',
'Utah', 'Vermont', 'Virginia',
'Washington', 'West Virginia', 'Wisconsin',
'Wyoming'],
options: [],
value: [],
}
},
}
</script>
```
:::
### Keyboard navigation
@ -392,6 +463,9 @@ Some APIs are still undergoing (comparing to the non-virtualized select), becaus
| autocomplete | select input 的 autocomplete 属性 | string | — | off |
| placeholder | the autocomplete attribute of select input | string | — | Please select |
| filterable | is filterable | boolean | — | false |
| filter-method | Méthode de filtrage personnalisée. | function | — | — |
| remote | Si les options sont chargées dynamiquement depuis le serveur. | boolean | — | false |
| remote-method | Méthode pour la recherche distante. | function | — | — |
| allow-create | Si l'utilisateur peut créer des options. Dans ce cas `filterable` doit être activé. | boolean | — | false |
| no-data-text | displayed text when there is no options, you can also use slot empty | string | — | No Data |
| popper-class | custom class name for Select's dropdown | string | — | — |

View File

@ -363,9 +363,80 @@ We can clear all the selected options at once, also applicable for single select
```
:::
### Remote search
### リモート検索
WIP 👷‍♀️
サーバーからキーワードや検索データを入力します。
:::demo リモート検索を有効にするには `filterable``remote``true` を設定し、`remote-method` を渡す必要がある。`remote-method`は入力値が変化したときに呼び出される `Function` であり、そのパラメータは現在の入力値である。
```html
<template>
<el-select-v2
v-model="value"
style="width:200px"
multiple
size="medium"
filterable
remote
:remote-method="remoteMethod"
clearable
:options="options"
:loading="loading"
placeholder="Please enter a keyword"
/>
</template>
<script>
export default {
created() {
this.list = this.states.map(item => {
return { value: `value:${item}`, label: `label:${item}` }
})
},
methods: {
remoteMethod(query) {
if (query !== '') {
this.loading = true
setTimeout(() => {
this.loading = false
this.options = this.list.filter(item => {
return item.label.toLowerCase()
.indexOf(query.toLowerCase()) > -1
})
}, 200)
} else {
this.options = []
}
},
},
data() {
return {
list: [],
loading: false,
states: ['Alabama', 'Alaska', 'Arizona',
'Arkansas', 'California', 'Colorado',
'Connecticut', 'Delaware', 'Florida',
'Georgia', 'Hawaii', 'Idaho', 'Illinois',
'Indiana', 'Iowa', 'Kansas', 'Kentucky',
'Louisiana', 'Maine', 'Maryland',
'Massachusetts', 'Michigan', 'Minnesota',
'Mississippi', 'Missouri', 'Montana',
'Nebraska', 'Nevada', 'New Hampshire',
'New Jersey', 'New Mexico', 'New York',
'North Carolina', 'North Dakota', 'Ohio',
'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina',
'South Dakota', 'Tennessee', 'Texas',
'Utah', 'Vermont', 'Virginia',
'Washington', 'West Virginia', 'Wisconsin',
'Wyoming'],
options: [],
value: [],
}
},
}
</script>
```
:::
### Keyboard navigation
@ -390,6 +461,9 @@ Some APIs are still undergoing (comparing to the non-virtualized select), becaus
| autocomplete | select input 的 autocomplete 属性 | string | — | off |
| placeholder | the autocomplete attribute of select input | string | — | Please select |
| filterable | is filterable | boolean | — | false |
| filter-method | カスタムフィルタ方式 | function | — | — |
| remote | オプションがサーバから読み込まれているかどうか | boolean | — | false |
| remote-method | カスタムリモート検索法 | function | — | — |
| allow-create | 新しいアイテムの作成を許可するかどうかを指定します。これを使うには、`filterable` がtrueでなければなりません。 | boolean | — | false |
| no-data-text | displayed text when there is no options, you can also use slot empty | string | — | No Data |
| popper-class | custom class name for Select's dropdown | string | — | — |

View File

@ -366,7 +366,77 @@
### 远程搜索
WIP (该功能还在施工中👷‍♀️)
从服务器搜索数据,输入关键字进行查找
:::demo 为了启用远程搜索,需要将`filterable`和`remote`设置为`true`,同时传入一个`remote-method`。`remote-method`为一个`Function`,它会在输入值发生变化时调用,参数为当前输入值。
```html
<template>
<el-select-v2
v-model="value"
style="width:200px"
multiple
size="medium"
filterable
remote
:remote-method="remoteMethod"
clearable
:options="options"
:loading="loading"
placeholder="请输入关键词"
/>
</template>
<script>
export default {
created() {
this.list = this.states.map(item => {
return { value: `value:${item}`, label: `label:${item}` }
})
},
methods: {
remoteMethod(query) {
if (query !== '') {
this.loading = true
setTimeout(() => {
this.loading = false
this.options = this.list.filter(item => {
return item.label.toLowerCase()
.indexOf(query.toLowerCase()) > -1
})
}, 200)
} else {
this.options = []
}
},
},
data() {
return {
list: [],
loading: false,
states: ['Alabama', 'Alaska', 'Arizona',
'Arkansas', 'California', 'Colorado',
'Connecticut', 'Delaware', 'Florida',
'Georgia', 'Hawaii', 'Idaho', 'Illinois',
'Indiana', 'Iowa', 'Kansas', 'Kentucky',
'Louisiana', 'Maine', 'Maryland',
'Massachusetts', 'Michigan', 'Minnesota',
'Mississippi', 'Missouri', 'Montana',
'Nebraska', 'Nevada', 'New Hampshire',
'New Jersey', 'New Mexico', 'New York',
'North Carolina', 'North Dakota', 'Ohio',
'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina',
'South Dakota', 'Tennessee', 'Texas',
'Utah', 'Vermont', 'Virginia',
'Washington', 'West Virginia', 'Wisconsin',
'Wyoming'],
options: [],
value: [],
}
},
}
</script>
```
:::
### 键盘操作
@ -391,6 +461,9 @@ WIP (该功能还在施工中👷‍♀️)
| autocomplete | select input 的 autocomplete 属性 | string | — | off |
| placeholder | 占位符 | string | — | 请选择 |
| filterable | 是否可搜索 | boolean | — | false |
| filter-method | 自定义搜索方法 | function | — | — |
| remote | 是否为远程搜索 | boolean | — | false |
| remote-method | 远程搜索方法 | function | — | — |
| allow-create | 是否允许用户创建新条目,需配合 `filterable` 使用 | boolean | — | false |
| no-data-text | 选项为空时显示的文字,也可以使用`#empty`设置 | string | — | 无数据 |
| popper-class | Select 下拉框的类名 | string | — | — |