Skip to content

Commit

Permalink
feat(SelectMenu): allow creating option despite search (#1080)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
ineshbose and benjamincanac committed Dec 15, 2023
1 parent 23770d8 commit 0fdc8f7
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const labels = computed({
// In a real app, you would make an API call to create the label
const response = {
id: options.value.length + 1,
name: label.name,
color: generateColorFromString(label.name)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
16 changes: 16 additions & 0 deletions docs/content/3.forms/4.select-menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

You can also configure this globally through the `ui.selectMenu.default.clearSearchOnClose` config. Defaults to `false`.

::component-card
---
baseProps:
Expand Down Expand Up @@ -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

Use the `popper` prop to customize the popper instance.
Expand Down
31 changes: 24 additions & 7 deletions src/runtime/components/forms/SelectMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@
</li>
</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]">
<div :class="uiMenu.option.container">
<slot name="option-create" :option="queryOption" :active="active" :selected="selected">
<span :class="uiMenu.option.create">Create "{{ queryOption[optionAttribute] }}"</span>
<slot name="option-create" :option="createOption" :active="active" :selected="selected">
<span :class="uiMenu.option.create">Create "{{ createOption[optionAttribute] }}"</span>
</slot>
</div>
</li>
Expand Down Expand Up @@ -247,7 +247,7 @@ export default defineComponent({
},
clearSearchOnClose: {
type: Boolean,
default: () => configMenu.default.clearOnClose
default: () => configMenu.default.clearSearchOnClose
},
debounce: {
type: Number,
Expand All @@ -257,6 +257,10 @@ export default defineComponent({
type: Boolean,
default: false
},
showCreateOptionWhen: {
type: String as PropType<'always' | 'empty'>,
default: () => configMenu.default.showCreateOptionWhen
},
placeholder: {
type: String,
default: null
Expand Down Expand Up @@ -438,8 +442,21 @@ export default defineComponent({
})
})
const queryOption = computed(() => {
return query.value === '' ? null : { [props.optionAttribute]: query.value }
const createOption = computed(() => {
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 () {
Expand Down Expand Up @@ -494,7 +511,7 @@ export default defineComponent({
trailingIconClass,
trailingWrapperIconClass,
filteredOptions,
queryOption,
createOption,
query,
onUpdate
}
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/ui.config/forms/selectMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export default {
},
default: {
selectedIcon: 'i-heroicons-check-20-solid',
clearOnClose: false
clearSearchOnClose: false,
showCreateOptionWhen: 'empty'
},
arrow: {
...arrow,
Expand Down

1 comment on commit 0fdc8f7

@vercel
Copy link

@vercel vercel bot commented on 0fdc8f7 Dec 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ui – ./

ui-git-dev-nuxt-js.vercel.app
ui.nuxt.com
ui-nuxt-js.vercel.app

Please sign in to comment.