feat(components): [select-v2] add props attribute (#14536)

* feat(components): [select-v2] add props attribute

* docs: updata

* docs: updata

* docs: updata
This commit is contained in:
qiang
2023-10-25 20:33:07 +08:00
committed by GitHub
parent 991496d850
commit 941966f976
12 changed files with 213 additions and 138 deletions

View File

@@ -3,7 +3,7 @@ title: Virtualized Select
lang: en-US
---
# <ElBadge value="beta">Select V2 virtualized selector</ElBadge>
# Select V2 virtualized selector ^(beta)
:::tip
@@ -131,19 +131,39 @@ select-v2/remote-search
:::
## use value-key
## Use value-key attribute
:::demo when `options.value` is an object, you should set a unique identity key name for value
when `options.value` is an object, you should set a unique identity key name for value
::: tip
Before ^(2.4.0), `value-key` was used both as the unique value of the selected object and as an alias for the value in `options`. Now `value-key` is only used as the unique value of the selected object, and the alias for the value in options is `props.value`.
:::
:::demo
select-v2/use-valueKey
:::
## Aliases for custom options ^(2.4.2)
When your `options` format is different from the default format, you can customize the alias of the `options` through the `props` attribute
:::demo
select-v2/props
:::
## SelectV2 Attributes
| Name | Description | Type | Accepted Values | Default |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------- |
| model-value / v-model | biding value | string / number / boolean / object | — | — |
| options | data of the options, the key of `value` and `label` can be customize by `props` | Array | — | — |
| props ^(2.4.2) | configuration options, see the following table | object | — | — |
| multiple | is multiple | boolean | — | false |
| disabled | is disabled | boolean | — | false |
| value-key | unique identity key name for value, required when value is an object | string | — | value |
@@ -183,6 +203,15 @@ select-v2/use-valueKey
| loading-text | 远程加载时显示的文字 | string | — | 加载中 | -->
</span>
## props
| Attribute | Description | Type | Default |
| --------- | --------------------------------------------------------------- | ------ | -------- |
| value | specify which key of node object is used as the node's value | string | value |
| label | specify which key of node object is used as the node's label | string | label |
| options | specify which key of node object is used as the node's children | string | options |
| disabled | specify which key of node object is used as the node's disabled | string | disabled |
## SelectV2 Events
| Name | Description | Params |

View File

@@ -0,0 +1,25 @@
<template>
<el-select-v2
v-model="value"
:options="options"
:props="props"
placeholder="Please select"
style="width: 240px"
filterable
multiple
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const initials = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
const props = {
label: 'name',
value: 'id',
}
const value = ref([])
const options = Array.from({ length: 1000 }).map((_, idx) => ({
id: `Option ${idx + 1}`,
name: `${initials[idx % 10]}${idx}`,
}))
</script>

View File

@@ -1,26 +1,28 @@
import { placements } from '@popperjs/core'
import { definePropType, isValidComponentSize } from '@element-plus/utils'
import { useSizeProp } from '@element-plus/hooks'
import { buildProps, definePropType, iconPropType } from '@element-plus/utils'
import { useTooltipContentProps } from '@element-plus/components/tooltip'
import { CircleClose } from '@element-plus/icons-vue'
import type { Component, PropType } from 'vue'
import type { ComponentSize } from '@element-plus/constants'
import type { OptionType } from './select.types'
import { defaultProps } from './useProps'
import type { Option, OptionType } from './select.types'
import type { Props } from './useProps'
import type { Options, Placement } from '@element-plus/components/popper'
export const SelectProps = {
export const SelectProps = buildProps({
allowCreate: Boolean,
autocomplete: {
type: String as PropType<'none' | 'both' | 'list' | 'inline'>,
type: definePropType<'none' | 'both' | 'list' | 'inline'>(String),
default: 'none',
},
automaticDropdown: Boolean,
clearable: Boolean,
clearIcon: {
type: [String, Object] as PropType<string | Component>,
type: iconPropType,
default: CircleClose,
},
effect: {
type: String as PropType<'light' | 'dark' | string>,
type: definePropType<'light' | 'dark' | string>(String),
default: 'light',
},
collapseTags: Boolean,
@@ -52,9 +54,11 @@ export const SelectProps = {
loading: Boolean,
loadingText: String,
label: String,
modelValue: [Array, String, Number, Boolean, Object] as PropType<
any[] | string | number | boolean | Record<string, any> | any
>,
modelValue: {
type: definePropType<
any[] | string | number | boolean | Record<string, any> | any
>([Array, String, Number, Boolean, Object]),
},
multiple: Boolean,
multipleLimit: {
type: Number,
@@ -69,7 +73,7 @@ export const SelectProps = {
default: true,
},
options: {
type: Array as PropType<OptionType[]>,
type: definePropType<OptionType[]>(Array),
required: true,
},
placeholder: {
@@ -85,13 +89,14 @@ export const SelectProps = {
default: '',
},
popperOptions: {
type: Object as PropType<Partial<Options>>,
type: definePropType<Partial<Options>>(Object),
default: () => ({} as Partial<Options>),
},
remote: Boolean,
size: {
type: String as PropType<ComponentSize>,
validator: isValidComponentSize,
size: useSizeProp,
props: {
type: definePropType<Props>(Object),
default: () => defaultProps,
},
valueKey: {
type: String,
@@ -110,15 +115,18 @@ export const SelectProps = {
values: placements,
default: 'bottom-start',
},
}
} as const)
export const OptionProps = {
export const OptionProps = buildProps({
data: Array,
disabled: Boolean,
hovering: Boolean,
item: Object,
item: {
type: definePropType<Option>(Object),
required: true,
},
index: Number,
style: Object,
selected: Boolean,
created: Boolean,
}
} as const)

View File

@@ -13,28 +13,33 @@
@click.stop="selectOptionClick"
>
<slot :item="item" :index="index" :disabled="disabled">
<span>{{ item.label }}</span>
<span>{{ getLabel(item) }}</span>
</slot>
</li>
</template>
<script lang="ts">
// @ts-nocheck
import { defineComponent } from 'vue'
import { defineComponent, inject } from 'vue'
import { useNamespace } from '@element-plus/hooks'
import { useOption } from './useOption'
import { useProps } from './useProps'
import { OptionProps } from './defaults'
import { selectV2InjectionKey } from './token'
export default defineComponent({
props: OptionProps,
emits: ['select', 'hover'],
setup(props, { emit }) {
const select = inject(selectV2InjectionKey)!
const ns = useNamespace('select')
const { hoverItem, selectOptionClick } = useOption(props, { emit })
const { getLabel } = useProps(select.props)
return {
ns,
hoverItem,
selectOptionClick,
getLabel,
}
},
})

View File

@@ -17,6 +17,7 @@ import { useNamespace } from '@element-plus/hooks'
import { EVENT_CODE } from '@element-plus/constants'
import GroupItem from './group-item.vue'
import OptionItem from './option-item.vue'
import { useProps } from './useProps'
import { selectV2InjectionKey } from './token'
@@ -37,6 +38,8 @@ export default defineComponent({
setup(props, { slots, expose }) {
const select = inject(selectV2InjectionKey)!
const ns = useNamespace('select')
const { getLabel, getValue, getDisabled } = useProps(select.props)
const cachedHeights = ref<Array<number>>([])
const listRef = ref()
@@ -92,9 +95,9 @@ export default defineComponent({
const isItemSelected = (modelValue: any[] | any, target: Option) => {
if (select.props.multiple) {
return contains(modelValue, target.value)
return contains(modelValue, getValue(target))
}
return isEqual(modelValue, target.value)
return isEqual(modelValue, getValue(target))
}
const isItemDisabled = (modelValue: any[] | any, selected: boolean) => {
@@ -159,7 +162,7 @@ export default defineComponent({
<OptionItem
{...itemProps}
selected={isSelected}
disabled={item.disabled || isDisabled}
disabled={getDisabled(item) || isDisabled}
created={!!item.created}
hovering={isHovering}
item={item}
@@ -168,7 +171,7 @@ export default defineComponent({
>
{{
default: (props: OptionItemProps) =>
slots.default?.(props) || <span>{item.label}</span>,
slots.default?.(props) || <span>{getLabel(item)}</span>,
}}
</OptionItem>
)

View File

@@ -1,25 +1,13 @@
export type OptionCommon = {
label: string
}
type OptionCommon = Record<string, any>
export type Option<T = any> = OptionCommon & {
export type Option = OptionCommon & {
created?: boolean
value: T
// reserve for option
[prop: string]: any
}
export type OptionGroup<T = any> = OptionCommon & {
options: Array<T>
export type OptionGroup = OptionCommon
// reserve for flexibility
[prop: string]: any
}
export type OptionType = Option | OptionGroup
export type OptionType<T = any> = Option<T> | OptionGroup<T>
// maybe adding T for type restriction is better here, but not sure this is going to work for
// template rendering
export type OptionItemProps = {
item: any
index: number

View File

@@ -43,11 +43,11 @@
<template v-if="collapseTags && modelValue.length > 0">
<div
v-for="item in showTagList"
:key="getValueKey(item)"
:key="getValueKey(getValue(item))"
:class="nsSelectV2.e('selected-item')"
>
<el-tag
:closable="!selectDisabled && !item?.disable"
:closable="!selectDisabled && !getDisabled(item)"
:size="collapseTagSize"
type="info"
disable-transitions
@@ -58,8 +58,9 @@
:style="{
maxWidth: `${tagMaxWidth}px`,
}"
>{{ item?.label }}</span
>
{{ getLabel(item) }}
</span>
</el-tag>
</div>
<div :class="nsSelectV2.e('selected-item')">
@@ -84,18 +85,21 @@
:style="{
maxWidth: `${tagMaxWidth}px`,
}"
>+ {{ modelValue.length - maxCollapseTags }}</span
>
+ {{ modelValue.length - maxCollapseTags }}
</span>
</template>
<template #content>
<div :class="nsSelectV2.e('selection')">
<div
v-for="selected in collapseTagList"
:key="getValueKey(selected)"
:key="getValueKey(getValue(selected))"
:class="nsSelectV2.e('selected-item')"
>
<el-tag
:closable="!selectDisabled && !selected.disabled"
:closable="
!selectDisabled && !getDisabled(selected)
"
:size="collapseTagSize"
class="in-tooltip"
type="info"
@@ -107,8 +111,9 @@
:style="{
maxWidth: `${tagMaxWidth}px`,
}"
>{{ getLabel(selected) }}</span
>
{{ getLabel(selected) }}
</span>
</el-tag>
</div>
</div>
@@ -120,8 +125,9 @@
:style="{
maxWidth: `${tagMaxWidth}px`,
}"
>+ {{ modelValue.length - maxCollapseTags }}</span
>
+ {{ modelValue.length - maxCollapseTags }}
</span>
</el-tag>
</div>
</template>
@@ -129,11 +135,11 @@
<template v-else>
<div
v-for="selected in states.cachedOptions"
:key="getValueKey(selected)"
:key="getValueKey(getValue(selected))"
:class="nsSelectV2.e('selected-item')"
>
<el-tag
:closable="!selectDisabled && !selected.disabled"
:closable="!selectDisabled && !getDisabled(selected)"
:size="collapseTagSize"
type="info"
disable-transitions
@@ -144,8 +150,9 @@
:style="{
maxWidth: `${tagMaxWidth}px`,
}"
>{{ getLabel(selected) }}</span
>
{{ getLabel(selected) }}
</span>
</el-tag>
</div>
</template>

View File

@@ -7,7 +7,7 @@ export interface SelectV2Context {
props: ExtractPropTypes<typeof SelectProps>
expanded: boolean
popper: Ref<TooltipInstance>
onSelect: (option: Option<any>, index: number, byClick?: boolean) => void
onSelect: (option: Option, index: number, byClick?: boolean) => void
onHover: (idx: number) => void
onKeyboardNavigate: (direction: 'forward' | 'backward') => void
onKeyboardSelect: () => void

View File

@@ -1,9 +1,12 @@
// @ts-nocheck
import { computed, ref } from 'vue'
import { useProps } from './useProps'
import type { ISelectProps } from './token'
import type { Option } from './select.types'
export function useAllowCreate(props: ISelectProps, states) {
const { aliasProps, getLabel, getValue } = useProps(props)
const createOptionCount = ref(0)
const cachedSelectedOption = ref<Option>(null)
@@ -12,7 +15,7 @@ export function useAllowCreate(props: ISelectProps, states) {
})
function hasExistingOption(query: string) {
const hasValue = (option) => option.value === query
const hasValue = (option) => getValue(option) === query
return (
(props.options && props.options.some(hasValue)) ||
states.createdOptions.some(hasValue)
@@ -34,10 +37,10 @@ export function useAllowCreate(props: ISelectProps, states) {
if (enableAllowCreateMode.value) {
if (query && query.length > 0 && !hasExistingOption(query)) {
const newOption = {
value: query,
label: query,
[aliasProps.value.value]: query,
[aliasProps.value.label]: query,
created: true,
disabled: false,
[aliasProps.value.disabled]: false,
}
if (states.createdOptions.length >= createOptionCount.value) {
states.createdOptions[createOptionCount.value] = newOption
@@ -65,12 +68,12 @@ export function useAllowCreate(props: ISelectProps, states) {
!option.created ||
(option.created &&
props.reserveKeyword &&
states.inputValue === option.label)
states.inputValue === getLabel(option))
) {
return
}
const idx = states.createdOptions.findIndex(
(it) => it.value === option.value
(it) => getValue(it) === getValue(option)
)
if (~idx) {
states.createdOptions.splice(idx, 1)

View File

@@ -0,0 +1,36 @@
import { computed } from 'vue'
import { get } from 'lodash-unified'
import type { ISelectProps } from './token'
import type { Option } from './select.types'
export interface Props {
label?: string
value?: string
disabled?: string
options?: string
}
export const defaultProps: Required<Props> = {
label: 'label',
value: 'value',
disabled: 'disabled',
options: 'options',
}
export function useProps(props: Pick<ISelectProps, 'props'>) {
const aliasProps = computed(() => ({ ...defaultProps, ...props.props }))
const getLabel = (option: Option) => get(option, aliasProps.value.label)
const getValue = (option: Option) => get(option, aliasProps.value.value)
const getDisabled = (option: Option) => get(option, aliasProps.value.disabled)
const getOptions = (option: Option) => get(option, aliasProps.value.options)
return {
aliasProps,
getLabel,
getValue,
getDisabled,
getOptions,
}
}

View File

@@ -14,14 +14,13 @@ import { useFormItem, useFormSize } from '@element-plus/components/form'
import { ArrowUp } from '@element-plus/icons-vue'
import { useAllowCreate } from './useAllowCreate'
import { flattenOptions } from './util'
import { useInput } from './useInput'
import { useProps } from './useProps'
import type { CSSProperties } from 'vue'
import type ElTooltip from '@element-plus/components/tooltip'
import type { SelectProps } from './defaults'
import type { CSSProperties, ExtractPropTypes } from 'vue'
import type { Option, OptionType } from './select.types'
import type { ISelectProps } from './token'
const DEFAULT_INPUT_PLACEHOLDER = ''
const MINIMUM_INPUT_WIDTH = 11
@@ -31,12 +30,13 @@ const TAG_BASE_WIDTH = {
small: 33,
}
const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
const useSelect = (props: ISelectProps, emit) => {
// inject
const { t } = useLocale()
const nsSelectV2 = useNamespace('select-v2')
const nsInput = useNamespace('input')
const { form: elForm, formItem: elFormItem } = useFormItem()
const { getLabel, getValue, getDisabled, getOptions } = useProps(props)
const states = reactive({
inputValue: DEFAULT_INPUT_PLACEHOLDER,
@@ -143,46 +143,49 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
const query = states.inputValue
// when query was given, we should test on the label see whether the label contains the given query
const regexp = new RegExp(escapeStringRegexp(query), 'i')
const containsQueryString = query ? regexp.test(o.label || '') : true
const containsQueryString = query ? regexp.test(getLabel(o) || '') : 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)
if (filtered.length > 0) {
return {
...v,
options: filtered,
}
}
} else {
if (props.remote || isValidOption(v as Option)) {
return v
}
}
return null
})
.filter((v) => v !== null)
)
return [...props.options, ...states.createdOptions].reduce((all, item) => {
const options = getOptions(item)
if (isArray(options)) {
const filtered = options.filter(isValidOption)
if (filtered.length > 0) {
all.push(
{
label: getLabel(item),
isTitle: true,
type: 'Group',
},
...filtered,
{ type: 'Group' }
)
}
} else if (props.remote || isValidOption(item)) {
all.push(item)
}
return all
}, []) as OptionType[]
})
const filteredOptionsValueMap = computed(() => {
const valueMap = new Map()
filteredOptions.value.forEach((option, index) => {
valueMap.set(getValueKey(option.value), { option, index })
valueMap.set(getValueKey(getValue(option)), { option, index })
})
return valueMap
})
const optionsAllDisabled = computed(() =>
filteredOptions.value.every((option) => option.disabled)
filteredOptions.value.every((option) => getDisabled(option))
)
const selectSize = useFormSize()
@@ -348,7 +351,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
const update = (val: any) => {
emit(UPDATE_MODEL_EVENT, val)
emitChange(val)
states.previousValue = val?.toString()
states.previousValue = String(val)
}
const getValueIndex = (arr = [], value: unknown) => {
@@ -371,12 +374,6 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
return isObject(item) ? get(item, props.valueKey) : item
}
// if the selected item is item then we get label via indexing
// otherwise it should be string we simply return the item itself.
const getLabel = (item: unknown) => {
return isObject(item) ? item.label : item
}
const resetInputHeight = () => {
return nextTick(() => {
if (!inputRef.value) return
@@ -409,7 +406,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
if (props.multiple) {
let selectedOptions = (props.modelValue as any[]).slice()
const index = getValueIndex(selectedOptions, option.value)
const index = getValueIndex(selectedOptions, getValue(option))
if (index > -1) {
selectedOptions = [
...selectedOptions.slice(0, index),
@@ -421,7 +418,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
props.multipleLimit <= 0 ||
selectedOptions.length < props.multipleLimit
) {
selectedOptions = [...selectedOptions, option.value]
selectedOptions = [...selectedOptions, getValue(option)]
states.cachedOptions.push(option)
selectNewOption(option)
updateHoveringIndex(idx)
@@ -444,8 +441,8 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
setSoftFocus()
} else {
selectedIndex.value = idx
states.selectedLabel = option.label
update(option.value)
states.selectedLabel = getLabel(option)
update(getValue(option))
expanded.value = false
states.isComposing = false
states.isSilentBlur = byClick
@@ -460,7 +457,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
const deleteTag = (event: MouseEvent, option: Option) => {
let selectedOptions = (props.modelValue as any[]).slice()
const index = getValueIndex(selectedOptions, option.value)
const index = getValueIndex(selectedOptions, getValue(option))
if (index > -1 && !selectDisabled.value) {
selectedOptions = [
@@ -469,7 +466,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
]
states.cachedOptions.splice(index, 1)
update(selectedOptions)
emit('remove-tag', option.value)
emit('remove-tag', getValue(option))
states.softFocus = true
removeNewOption(option)
return nextTick(focusAndUpdatePopup)
@@ -589,7 +586,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
}
}
const option = options[newIndex]
if (option.disabled || option.type === 'Group') {
if (getDisabled(option) || option.type === 'Group') {
// prevent dispatching multiple nextTick callbacks.
return onKeyboardNavigate(direction, newIndex)
} else {
@@ -697,10 +694,10 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
const options = filteredOptions.value
const selectedItemIndex = options.findIndex(
(option) =>
getValueKey(option.value) === getValueKey(props.modelValue)
getValueKey(getValue(option)) === getValueKey(props.modelValue)
)
if (~selectedItemIndex) {
states.selectedLabel = options[selectedItemIndex].label
states.selectedLabel = getLabel(options[selectedItemIndex])
updateHoveringIndex(selectedItemIndex)
} else {
states.selectedLabel = `${props.modelValue}`
@@ -823,6 +820,8 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
debouncedOnInputChange,
deleteTag,
getLabel,
getValue,
getDisabled,
getValueKey,
handleBlur,
handleClear,

View File

@@ -1,28 +0,0 @@
// @ts-nocheck
import { isArray } from '@vue/shared'
import type { Option, OptionGroup } from './select.types'
export const flattenOptions = (options: Array<Option | OptionGroup>) => {
const flattened = []
options.forEach((option) => {
if (isArray(option.options)) {
flattened.push({
label: option.label,
isTitle: true,
type: 'Group',
})
option.options.forEach((o: Option) => {
flattened.push(o)
})
flattened.push({
type: 'Group',
})
} else {
flattened.push(option)
}
})
return flattened
}