vuetifyを使用してoptgroup付きのselectboxを作成したときのメモです。
やりたいこと
- multiple selectの場合はチェックボックスを表示し、optgroupも選択可能にする
- optgroupをクリックしたときには配下のitemを全て選択したことにして非活性にする
- 検索処理や更新処理など幅広く使えるようにしたいので、状況に合わせて使い分けられるように、以下の3パターンのデータを返却させる
- groupの選択状況
- itemの選択状況
- groupもitemに置き換えた場合のitemの選択状況
参考元
v-select Slots
list-item-groups selection-controls
GroupedSelect.vue
template
v-autocompleteのitem slotを拡張する方法で作成します。
また、選択結果にはv-chipを表示させたいのでdata slotにも手を入れます。
<template>
<v-autocomplete
v-model="selectedValues"
v-bind="$attrs"
color="primary"
item-text="text"
item-value="value"
class="grouped-select"
:placeholder="placeholder.selectable"
:items="innerItems"
:multiple="multiple"
chips
>
<template #label>
<slot name="label" />
</template>
<template #selection="data">
<template v-if="multiple">
<v-chip
v-if="!(singleLine && data.index > 0)"
class="chip--select-multi px-2"
:input-value="data.selected"
:color="isGroupOption(data.item) ? 'secondary darken-2' : 'primary'"
:close="!singleLine"
outlined
small
@click:close="removeItem(data.item)"
>
{{ data.item.text }}
</v-chip>
<v-badge
v-if="singleLine && data.index > 0"
:content="`他${data.index}`"
:color="isGroupOption(data.item) ? 'secondary darken-2' : 'primary'"
absolute
top
left
/>
</template>
<template v-else>
{{ data.item.text }}
</template>
</template>
<template #item="{ item, attrs, on }">
<v-list-item
v-slot="{ active }"
v-bind="attrs"
class="px-1"
:class="{ 'ml-6': isItemOption(item) && hasGroup }"
:disabled="isGroupOption(item) ? !multiple : isGroupParentSelected(item)"
v-on="on"
>
<v-list-item-action v-if="multiple" class="mr-2">
<v-checkbox
:disabled="isGroupOption(item) ? !multiple : isGroupParentSelected(item)"
:input-value="active"
/>
</v-list-item-action>
<v-list-item-content
class="ml-2 text-caption"
@click="clickItem(item)"
v-text="item.text"
/>
</v-list-item>
</template>
</v-autocomplete>
</template>
type
GroupedSelectValuesは選択結果のtype定義、GroupedSelectItemは選択肢の公開用type定義です。
export interface GroupedSelectValues {
groupIdList: string[]
itemIdList: string[]
idList: string[]
}
interface GroupedSelectOption {
optionType: 'group' | 'item'
itemId: string
groupId: string
text: string
value: string
}
export type GroupedSelectItem = Omit<GroupedSelectOption, 'value'>
props, data
groupIdList, itemIdListでそれぞれの選択状況を指定します。
props: {
multiple: {
type: Boolean,
default: false
},
singleLine: {
type: Boolean,
default: false
},
items: {
type: Array as PropType<GroupedSelectItem[]>,
default: () => []
},
groupIdList: {
type: Array as PropType<string[]>,
default: () => []
},
itemIdList: {
type: Array as PropType<string[]>,
default: () => []
},
},
data () {
return {
selected: this.multiple ? [] : undefined
}
},
computed
選択状況の getter, setter
selectedValues: {
get (): string[] | string | undefined {
return this.selected
},
set (v: string[] | string | undefined): void {
if (v instanceof Array && v.length === this.selected?.length) {
this.selected = v
} else {
this.$emit('change', this.getResult(v))
this.selected = v
}
}
},
${e.optionType}-${e.itemId}
のルールで内部管理用のidを割り振ります。
innerItems (): GroupedSelectOption[] {
return (this.items || []).map((e) =>
({ ...e, value: `${e.optionType}-${e.itemId}` }))
},
状態管理系処理
hasGroup (): boolean {
return !!(this.items || []).find(this.isGroupOption)
},
isGroupParentSelected () {
const selected = this.selected
return (item: GroupedSelectOption) =>
castArray(selected).includes(`group-${item.groupId}`)
},
watch
選択状況の変更監視
外部から選択状況が変更された場合のフォロー用です。
groupIdList (val) {
this.refreshSelectedValues(
castArray(this.itemIdList),
castArray(val))
},
itemIdList (val) {
this.refreshSelectedValues(
castArray(val),
castArray(this.groupIdList))
}
created
created () {
this.refreshSelectedValues(
castArray(this.itemIdList),
castArray(this.groupIdList))
},
methods
選択状況を内部管理idに変換します。
refreshSelectedValues (itemIdList: string[], groupIdList: string[]) {
if (this.multiple) {
this.selectedValues = [
...groupIdList.map(v => `group-${v}`),
...(itemIdList).map(v => `item-${v}`)
]
} else {
this.selectedValues = itemIdList[0] ? `item-${itemIdList[0]}` : undefined
}
},
状態管理系処理
isGroupOption (item: GroupedSelectItem) {
return item.optionType === 'group'
},
isItemOption (item: GroupedSelectItem) {
return item.optionType === 'item'
},
groupの子itemを取得します。
getGroupedItemList (group: GroupedSelectItem): GroupedSelectOption[] {
if (this.isGroupOption(group)) {
return this.innerItems.filter((item) =>
this.isItemOption(item) && item.groupId === group.groupId)
} else {
return []
}
},
選択削除処理
removeItem (item: GroupedSelectOption) {
if (this.selectedValues instanceof Array) {
this.selectedValues = this.selectedValues.filter((v) => v !== item.value)
} else {
this.selectedValues = undefined
}
},
選択時処理
groupの場合、子itemの選択を外します。
clickItem (item: GroupedSelectItem) {
if (this.multiple === true && this.isGroupOption(item)) {
this.getGroupedItemList(item).forEach(this.removeItem)
}
},
選択状況を作成します。
- groupIdList: groupの選択中idリスト
- itemIdList: itemの選択中idリスト
- idList: 選択状況を全てitemで表現した場合の選択中idリスト
getResult (selected: string[] | string | undefined): GroupedSelectValues {
return castArray(selected).reduce((prev: GroupedSelectValues, value) => {
const item = this.innerItems.find((item) => item.value === value)
if (item && this.isGroupOption(item)) {
prev.groupIdList.push(item.groupId)
prev.idList = union(
prev.idList,
this.getGroupedItemList(item).map(e => e.itemId))
} else if (item && this.isItemOption(item)) {
prev.itemIdList.push(item.itemId)
prev.idList = union(prev.idList, [item.itemId])
}
return prev
}, {groupIdList: [], itemIdList: [], idList: []})
},
style
<style lang="scss" scoped>
.grouped-select ::v-deep .v-select__slot {
.v-input__append-inner {
margin-top: 10px !important;
padding: 0;
.v-input__icon--append,
.v-input__icon--clear {
width: 20px;
min-width: 20px;
height: 20px;
font-size: 20px;
i,
button
{
font-size: 20px;
}
}
}
.v-select__selections > * {
margin-top: 4px;
&:is(input) {
margin-left: 8px;
margin-right: 8px;
padding-left: 8px;
padding-right: 8px;
border-radius: 4px;
background-color: $ORG_BLUE2;
}
}
}
</style>
使い方 (component)
シンプルなリストボックスとして使用する例です。
複数選択か単体選択かはpropsで制御できるようにします。
Group1, Item5が選択された場合には、Item1 ~ Item5のid [ 'i1', 'i2', 'i3', 'i5' ]
が取得されることを期待します。
template
<template>
<grouped-select
v-bind="$attrs"
:items="items"
:group-id-list="castArray(value.group)"
:item-id-list="castArray(value.item)"
:multiple="multiple"
@change="change"
>
<template #label>
<slot name="label"/>
</template>
</grouped-select>
</template>
model, props
model: {
prop: 'value',
event: 'change',
},
props: {
multiple: {
type: Boolean,
default: false
},
value: {
type: Object as PropType<Value>,
default: undefined
},
},
vue2ではmodelがひとつしか使えないのでgroupとitemを保持できる構造にします。
export interface Value {
group: string | string[]
item : string | string[]
}
computed
選択肢の取得
items (): GroupedSelectItem[] {
// sample items
return [
{ optionType: 'group', itemId: 'g1', groupId: 'g1', text: 'Group1' },
{ optionType: 'item', itemId: 'i1', groupId: 'g1', text: 'Item1' },
{ optionType: 'item', itemId: 'i2', groupId: 'g1', text: 'Item2' },
{ optionType: 'item', itemId: 'i3', groupId: 'g1', text: 'Item3' },
{ optionType: 'group', itemId: 'g2', groupId: 'g2', text: 'Group2' },
{ optionType: 'item', itemId: 'i5', groupId: 'g2', text: 'Item5' },
{ optionType: 'item', itemId: 'i6', groupId: 'g2', text: 'Item6' },
]
},
methods
multiple selectの場合はstring[]、single selectの場合はstringで返却します。
例ではgroupの選択状況は不要でitemの選択状況だけがあれば良いのでidListだけを使用します。
change (values: GroupedSelectValues) {
if (this.multiple === true) {
const value: Value = {
group: values.groupIdList,
item : values.itemIdList,
}
this.$emit('change', value)
} else {
const value: Value = {
group: '',
item : values.idList[0],
}
this.$emit('change', value)
}
},
その他パーツ
import _castArray from "lodash/castArray"
export function castArray <T> (v: T[] | T | undefined) {
return V ? _castArray(v) : []
}