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

fix(SelectMenu): use by prop to compare objects for selected values & add support for dot notation in value-attribute #2228

Closed
wants to merge 9 commits into from
77 changes: 63 additions & 14 deletions src/runtime/components/forms/SelectMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
v-slot="{ active, selected: optionSelected, disabled: optionDisabled }"
:key="index"
as="template"
:value="valueAttribute ? option[valueAttribute] : option"
:value="valueAttribute ? accessor(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, optionSelected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
Expand Down Expand Up @@ -364,21 +364,72 @@ export default defineComponent({
})

const selected = computed(() => {
const options = props.options || []

// handle multiple modelValue options
if (props.multiple) {
if (!Array.isArray(props.modelValue) || !props.modelValue.length) {
const modelValue = props.modelValue
if (!Array.isArray(modelValue) || !modelValue.length) {
return []
}

if (props.valueAttribute) {
return options.value.filter(option => (props.modelValue as any[]).includes(option[props.valueAttribute]))
if (props.by) {
// array of objects compared based on the attribute provided by the `by` prop
return options.filter(
(_option) => {
const option = typeof _option === 'object' && _option !== null ? accessor(_option, props.valueAttribute) : null
return typeof option === 'object' && option !== null && modelValue.some(
(value: any) => typeof value === 'object' && value !== null && value[props.by] === option[props.by]
)
}
)
}

// array of items compared based on their value (or reference, in case of objects)
return options.filter(option =>
modelValue.includes(typeof option === 'object' && option !== null ? accessor(option, props.valueAttribute) : option)
)
}

if (props.by) {
return options.filter(
option => typeof option === 'object' && option !== null && modelValue.some(
(value: any) => typeof value === 'object' && value !== null && value[props.by] === option[props.by]
)
)
}

return options.filter(option => modelValue.includes(option))
} // end of multiple modelValue options handling

// handle single modelValue option of an `object` type
if (typeof props.modelValue === 'object' && props.modelValue !== null) {
if (props.valueAttribute) {
if (props.by) {
return options.find(
(_option) => {
const option = typeof _option === 'object' && _option !== null ? accessor(_option, props.valueAttribute) : null
return typeof option === 'object' && option !== null && option[props.by] === props.modelValue[props.by]
}
)
}

return options.find(option => (typeof option === 'object' && option !== null ? accessor(option, props.valueAttribute) : option) === toRaw(props.modelValue))
}
return options.value.filter(option => (props.modelValue as any[]).includes(option))
}

if (props.valueAttribute) {
return options.value.find(option => option[props.valueAttribute] === props.modelValue)
if (props.by) {
return options.find(option => (typeof option === 'object' && option !== null ? option[props.by] : option) === props.modelValue[props.by])
}
}
return options.value.find(option => option === props.modelValue)

// handle single modelValue option of a primitive type
return options.find(option =>
(typeof option === 'object' && option !== null && props.valueAttribute
? accessor(option, props.valueAttribute)
: option
) === toRaw(props.modelValue)
)
})

const label = computed(() => {
Expand All @@ -389,11 +440,9 @@ export default defineComponent({
return null
}
} else if (props.modelValue !== undefined && props.modelValue !== null) {
if (props.valueAttribute) {
return accessor(selected.value, props.optionAttribute) ?? null
} else {
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : accessor(props.modelValue as Record<string, any>, props.optionAttribute)
}
return typeof selected.value === 'object' && selected.value !== null && props.optionAttribute
? accessor(selected.value, props.optionAttribute) ?? null
: selected.value
}

return null
Expand Down Expand Up @@ -481,7 +530,7 @@ export default defineComponent({
}

return props.options || []
}, [], {
}, props.options || [], {
Copy link
Member Author

Choose a reason for hiding this comment

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

@benjamincanac Since the selected computed proeprty uses the computedAsync optionsΒ to get the possible values, I initialized it to props.optionsΒ so that it isn't empty during the initial render of the component to prevent a flash.
However, now that I'm thinking about it, shouldn't we just use props.options directly in the selectedΒ computed, instead of options - since we don't want to compare the model values with only the search results, but rather the whole options?

Copy link
Member Author

@cernymatej cernymatej Sep 20, 2024

Choose a reason for hiding this comment

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

I used options because that's what was used previously, however, unless I'm missing something, I think that props.options should have been used there as well:

image

Though this only affects things if someone uses asynchronous search.

lazy: props.searchableLazy
})

Expand Down