mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-02-02 13:17:57 +01:00
feat(SelectMenu): allow creating option despite search (#1080)
* chore: initial * chore: use reusable vnode * fix: use component with vnode * chore: option placement * chore: finish * up * up * up * fix(selectmenu): non-object custom options * up --------- Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
@@ -23,6 +23,7 @@ const labels = computed({
|
|||||||
|
|
||||||
// In a real app, you would make an API call to create the label
|
// In a real app, you would make an API call to create the label
|
||||||
const response = {
|
const response = {
|
||||||
|
id: options.value.length + 1,
|
||||||
name: label.name,
|
name: label.name,
|
||||||
color: generateColorFromString(label.name)
|
color: generateColorFromString(label.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup>
|
||||||
|
const options = ref([
|
||||||
|
{ id: 1, name: 'bug' },
|
||||||
|
{ id: 2, name: 'documentation' },
|
||||||
|
{ id: 3, name: 'duplicate' },
|
||||||
|
{ id: 4, name: 'enhancement' },
|
||||||
|
{ id: 5, name: 'good first issue' },
|
||||||
|
{ id: 6, name: 'help wanted' },
|
||||||
|
{ id: 7, name: 'invalid' },
|
||||||
|
{ id: 8, name: 'question' },
|
||||||
|
{ id: 9, name: 'wontfix' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const selected = ref([])
|
||||||
|
|
||||||
|
const labels = computed({
|
||||||
|
get: () => selected.value,
|
||||||
|
set: async (labels) => {
|
||||||
|
const promises = labels.map(async (label) => {
|
||||||
|
if (label.id) {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a real app, you would make an API call to create the label
|
||||||
|
const response = {
|
||||||
|
id: options.value.length + 1,
|
||||||
|
name: label.name
|
||||||
|
}
|
||||||
|
|
||||||
|
options.value.push(response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
|
||||||
|
selected.value = await Promise.all(promises)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelectMenu
|
||||||
|
v-model="labels"
|
||||||
|
by="id"
|
||||||
|
name="labels"
|
||||||
|
:options="options"
|
||||||
|
option-attribute="name"
|
||||||
|
multiple
|
||||||
|
searchable
|
||||||
|
creatable
|
||||||
|
show-create-option-when="always"
|
||||||
|
placeholder="Select labels"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -117,6 +117,8 @@ componentProps:
|
|||||||
|
|
||||||
By default, the search query will be kept after the menu is closed. To clear it on close, set the `clear-search-on-close` prop.
|
By default, the search query will be kept after the menu is closed. To clear it on close, set the `clear-search-on-close` prop.
|
||||||
|
|
||||||
|
You can also configure this globally through the `ui.selectMenu.default.clearSearchOnClose` config. Defaults to `false`.
|
||||||
|
|
||||||
::component-card
|
::component-card
|
||||||
---
|
---
|
||||||
baseProps:
|
baseProps:
|
||||||
@@ -158,6 +160,20 @@ componentProps:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
However, if you want to create options despite search query (apart from exact match), you can set the `show-create-option-when` prop to `'always'`.
|
||||||
|
|
||||||
|
You can also configure this globally through the `ui.selectMenu.default.showCreateOptionWhen` config. Defaults to `empty`.
|
||||||
|
|
||||||
|
Try to search for something that exists in the example below, but not an exact match.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
component: 'select-menu-example-creatable-always'
|
||||||
|
componentProps:
|
||||||
|
class: 'w-full lg:w-48'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
## Popper
|
## Popper
|
||||||
|
|
||||||
Use the `popper` prop to customize the popper instance.
|
Use the `popper` prop to customize the popper instance.
|
||||||
|
|||||||
@@ -98,11 +98,11 @@
|
|||||||
</li>
|
</li>
|
||||||
</component>
|
</component>
|
||||||
|
|
||||||
<component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template">
|
<component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && createOption" v-slot="{ active, selected }" :value="createOption" as="template">
|
||||||
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive]">
|
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive]">
|
||||||
<div :class="uiMenu.option.container">
|
<div :class="uiMenu.option.container">
|
||||||
<slot name="option-create" :option="queryOption" :active="active" :selected="selected">
|
<slot name="option-create" :option="createOption" :active="active" :selected="selected">
|
||||||
<span :class="uiMenu.option.create">Create "{{ queryOption[optionAttribute] }}"</span>
|
<span :class="uiMenu.option.create">Create "{{ createOption[optionAttribute] }}"</span>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -247,7 +247,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
clearSearchOnClose: {
|
clearSearchOnClose: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: () => configMenu.default.clearOnClose
|
default: () => configMenu.default.clearSearchOnClose
|
||||||
},
|
},
|
||||||
debounce: {
|
debounce: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -257,6 +257,10 @@ export default defineComponent({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
showCreateOptionWhen: {
|
||||||
|
type: String as PropType<'always' | 'empty'>,
|
||||||
|
default: () => configMenu.default.showCreateOptionWhen
|
||||||
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
@@ -438,8 +442,21 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const queryOption = computed(() => {
|
const createOption = computed(() => {
|
||||||
return query.value === '' ? null : { [props.optionAttribute]: query.value }
|
if (query.value === '') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (props.showCreateOptionWhen === 'empty' && filteredOptions.value.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (props.showCreateOptionWhen === 'always') {
|
||||||
|
const existingOption = filteredOptions.value.find(option => ['string', 'number'].includes(typeof option) ? option === query.value : option[props.optionAttribute] === query.value)
|
||||||
|
if (existingOption) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['string', 'number'].includes(typeof props.modelValue) ? query.value : { [props.optionAttribute]: query.value }
|
||||||
})
|
})
|
||||||
|
|
||||||
function clearOnClose () {
|
function clearOnClose () {
|
||||||
@@ -494,7 +511,7 @@ export default defineComponent({
|
|||||||
trailingIconClass,
|
trailingIconClass,
|
||||||
trailingWrapperIconClass,
|
trailingWrapperIconClass,
|
||||||
filteredOptions,
|
filteredOptions,
|
||||||
queryOption,
|
createOption,
|
||||||
query,
|
query,
|
||||||
onUpdate
|
onUpdate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export default {
|
|||||||
},
|
},
|
||||||
default: {
|
default: {
|
||||||
selectedIcon: 'i-heroicons-check-20-solid',
|
selectedIcon: 'i-heroicons-check-20-solid',
|
||||||
clearOnClose: false
|
clearSearchOnClose: false,
|
||||||
|
showCreateOptionWhen: 'empty'
|
||||||
},
|
},
|
||||||
arrow: {
|
arrow: {
|
||||||
...arrow,
|
...arrow,
|
||||||
|
|||||||
Reference in New Issue
Block a user