refactor: improve props handling for MultiSelect and SingleSelect components

MultiSelect:
- Remove explicit options prop to pass all PrimeVue props via v-bind="$attrs"
- Add defineOptions({ inheritAttrs: false }) for controlled attribute inheritance
- Extend PrimeVue MultiSelectProps type in Storybook for type safety
- Add component comments explaining differences from SingleSelect and prop requirements

SingleSelect:
- Keep options prop (required for getLabel function to map values to labels)
- Add v-bind="$attrs" to maximize PrimeVue API compatibility
- Add defineOptions({ inheritAttrs: false }) for controlled attribute inheritance
- Add component and function comments explaining why options prop is necessary

Common changes:
- Keep option-label="name" as custom option template directly references it
- Use label prop instead of placeholder
- Improve Storybook type definitions to resolve TypeScript compilation errors

This refactor allows MultiSelect to flexibly utilize PrimeVue APIs while
SingleSelect maintains necessary customizations with type safety.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jin Yi
2025-08-31 13:40:33 +09:00
parent 953cf56c4f
commit 55408ca9b1
4 changed files with 57 additions and 8 deletions

View File

@@ -1,9 +1,23 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import type { MultiSelectProps } from 'primevue/multiselect'
import { ref } from 'vue'
import MultiSelect from './MultiSelect.vue'
const meta: Meta<typeof MultiSelect> = {
// Combine our component props with PrimeVue MultiSelect props
// Since we use v-bind="$attrs", all PrimeVue props are available
interface ExtendedProps extends Partial<MultiSelectProps> {
// Our custom props
label?: string
showSearchBox?: boolean
showSelectedCount?: boolean
showClearButton?: boolean
searchPlaceholder?: string
// Override modelValue type to match our Option type
modelValue?: Array<{ name: string; value: string }>
}
const meta: Meta<ExtendedProps> = {
title: 'Components/Input/MultiSelect',
component: MultiSelect,
tags: ['autodocs'],

View File

@@ -1,10 +1,18 @@
<template>
<!--
Note: Unlike SingleSelect, we don't need an explicit options prop because:
1. Our value template only shows a static label (not dynamic based on selection)
2. We display a count badge instead of actual selected labels
3. All PrimeVue props (including options) are passed via v-bind="$attrs"
option-label="name" is required because our option template directly accesses option.name
max-selected-labels="0" is required to show count badge instead of selected item labels
-->
<MultiSelect
v-model="selectedItems"
:options="options"
v-bind="$attrs"
option-label="name"
unstyled
:placeholder="label"
:max-selected-labels="0"
:pt="pt"
>
@@ -102,11 +110,13 @@ import TextButton from '../button/TextButton.vue'
type Option = { name: string; value: string }
defineOptions({
inheritAttrs: false
})
interface Props {
/** Input label shown on the trigger button */
label?: string
/** Static options for the multiselect (when not using async search) */
options: Option[]
/** Show search box in the panel header */
showSearchBox?: boolean
/** Show selected count text in the panel header */
@@ -115,10 +125,11 @@ interface Props {
showClearButton?: boolean
/** Placeholder for the search input */
searchPlaceholder?: string
// Note: options prop is intentionally omitted.
// It's passed via $attrs to maximize PrimeVue API compatibility
}
const {
label,
options,
showSearchBox = false,
showSelectedCount = false,
showClearButton = false,

View File

@@ -4,6 +4,7 @@ import { ref } from 'vue'
import SingleSelect from './SingleSelect.vue'
// SingleSelect already includes options prop, so no need to extend
const meta: Meta<typeof SingleSelect> = {
title: 'Components/Input/SingleSelect',
component: SingleSelect,

View File

@@ -1,11 +1,19 @@
<template>
<!--
Note: We explicitly pass options here (not just via $attrs) because:
1. Our custom value template needs options to look up labels from values
2. PrimeVue's value slot only provides 'value' and 'placeholder', not the selected item's label
3. We need to maintain the icon slot functionality in the value template
option-label="name" is required because our option template directly accesses option.name
-->
<Select
v-model="selectedItem"
v-bind="$attrs"
:options="options"
option-label="name"
option-value="value"
unstyled
:placeholder="label"
:pt="pt"
>
<!-- Trigger value -->
@@ -48,9 +56,18 @@
import Select, { SelectPassThroughMethodOptions } from 'primevue/select'
import { computed } from 'vue'
defineOptions({
inheritAttrs: false
})
const { label, options } = defineProps<{
label?: string
options: {
/**
* Required for displaying the selected item's label.
* Cannot rely on $attrs alone because we need to access options
* in getLabel() to map values to their display names.
*/
options?: {
name: string
value: string
}[]
@@ -58,8 +75,14 @@ const { label, options } = defineProps<{
const selectedItem = defineModel<string | null>({ required: true })
/**
* Maps a value to its display label.
* Necessary because PrimeVue's value slot doesn't provide the selected item's label,
* only the raw value. We need this to show the correct text when an item is selected.
*/
const getLabel = (val: string | null | undefined) => {
if (val == null) return label ?? ''
if (!options) return label ?? ''
const found = options.find((o) => o.value === val)
return found ? found.name : label ?? ''
}