Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SelectMenu): add selected to label / leading / trailing slots props #1349

Merged
merged 7 commits into from
Jul 22, 2024
44 changes: 33 additions & 11 deletions src/runtime/components/forms/SelectMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
v-slot="{ open }"
:by="by"
:name="name"
:model-value="modelValue"
:model-value="multiple ? (Array.isArray(modelValue) ? modelValue : []) : modelValue"
:multiple="multiple"
:disabled="disabled"
as="div"
Expand Down Expand Up @@ -35,7 +35,7 @@
</slot>
</span>

<slot name="label">
<slot name="label" :selected="selected">
<span v-if="label" :class="uiMenu.label">{{ label }}</span>
<span v-else :class="uiMenu.label">{{ placeholder || '&nbsp;' }}</span>
</slot>
Expand Down Expand Up @@ -68,15 +68,15 @@
<component
:is="searchable ? 'HComboboxOption' : 'HListboxOption'"
v-for="(option, index) in filteredOptions"
v-slot="{ active, selected, disabled: optionDisabled }"
v-slot="{ active, selected: optionSelected, disabled: optionDisabled }"
:key="index"
as="template"
:value="valueAttribute ? option[valueAttribute] : option"
:disabled="option.disabled"
>
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive, selected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive, optionSelected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
<div :class="uiMenu.option.container">
<slot name="option" :option="option" :active="active" :selected="selected">
<slot name="option" :option="option" :active="active" :selected="optionSelected">
<UIcon v-if="option.icon" :name="option.icon" :class="[uiMenu.option.icon.base, active ? uiMenu.option.icon.active : uiMenu.option.icon.inactive, option.iconClass]" aria-hidden="true" />
<UAvatar
v-else-if="option.avatar"
Expand All @@ -90,16 +90,16 @@
</slot>
</div>

<span v-if="selected" :class="[uiMenu.option.selectedIcon.wrapper, uiMenu.option.selectedIcon.padding]">
<span v-if="optionSelected" :class="[uiMenu.option.selectedIcon.wrapper, uiMenu.option.selectedIcon.padding]">
<UIcon :name="selectedIcon" :class="uiMenu.option.selectedIcon.base" aria-hidden="true" />
</span>
</li>
</component>

<component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && createOption" v-slot="{ active, selected }" :value="createOption" as="template">
<component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && createOption" v-slot="{ active, selected: optionSelected }" :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="createOption" :active="active" :selected="selected">
<slot name="option-create" :option="createOption" :active="active" :selected="optionSelected">
<span :class="uiMenu.option.create">Create "{{ createOption[optionAttribute] }}"</span>
</slot>
</div>
Expand Down Expand Up @@ -335,6 +335,10 @@ export default defineComponent({
},
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change'],
setup (props, { emit, slots }) {
if (import.meta.dev && props.multiple && !Array.isArray(props.modelValue)) {
console.warn(`[@nuxt/ui] The USelectMenu components needs to have a modelValue of type Array when using the multiple prop. Got '${typeof props.modelValue}' instead.`, props.modelValue)
}

const { ui, attrs } = useUI('select', toRef(props, 'ui'), config, toRef(props, 'class'))
const { ui: uiMenu } = useUI('selectMenu', toRef(props, 'uiMenu'), configMenu)

Expand All @@ -358,17 +362,34 @@ export default defineComponent({
}
})

const selected = computed(() => {
Copy link
Member

Choose a reason for hiding this comment

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

Not sure to understand why you added this computed, could you provide a real-life example where you need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure!
When you change the selected value using the value-attribute prop, there is currently no way (at least as far as I'm aware) to access the remaining attributes of the selected object.
Let's say we have an array of categories with translated names:

const categories = [
  {
    id: 1,
    name: {
      en: 'Category',
      de: 'Kategorie',
      // ...
    },
  },
  // ...
]

This is how I would use it:

<USelectMenu
    :options="categories"
    value-attribute="id"
>
    <template #label="{ selected }">
        {{ useLocalizedValue(selected.name) }}
    </template>
</USelectMenu>

This applies to the #leading and #trailing slots as well. Currently, you wouldn't be able to access any icon attributes of the object in those slots, if you changed the value-attribute, would you? So it might be worth adding this to those slots as well.

<!--  example from the docs  -->
<template>
  <USelectMenu v-model="selected" :options="people"> <!--  if we add `value-attribute="id"`, this won't work  --> 
    <template #leading>
      <UIcon v-if="selected.icon" :name="(selected.icon as string)" class="w-4 h-4 mx-0.5" />
      <UAvatar v-else-if="selected.avatar" v-bind="(selected.avatar as Avatar)" size="3xs" class="mx-0.5" />
    </template>
  </USelectMenu>
</template>

Or am I missing something?

if (props.multiple) {
if (!Array.isArray(props.modelValue) || !props.modelValue.length) {
return []
}

if (props.valueAttribute) {
return options.value.filter(option => (props.modelValue as any[]).includes(option[props.valueAttribute]))
}
return options.value.filter(option => (props.modelValue as any[]).includes(option))
}

if (props.valueAttribute) {
return options.value.find(option => option[props.valueAttribute] === props.modelValue)
}
return options.value.find(option => option === props.modelValue)
})

const label = computed(() => {
if (props.multiple) {
if (Array.isArray(props.modelValue) && props.modelValue.length) {
return `${props.modelValue.length} selected`
return `${selected.value.length} selected`
} else {
return null
}
} else if (props.modelValue !== undefined && props.modelValue !== null) {
if (props.valueAttribute) {
const option = options.value.find(option => option[props.valueAttribute] === props.modelValue)
return option ? option[props.optionAttribute] : null
return selected.value?.[props.optionAttribute] ?? null
} else {
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : props.modelValue[props.optionAttribute]
}
Expand Down Expand Up @@ -543,6 +564,7 @@ export default defineComponent({
popper,
trigger,
container,
selected,
label,
isLeading,
isTrailing,
Expand Down