Vuetify2で作成した、v-data-table:ボタンクリック(expand)で複数行の内訳の表示・非表示を切り替える をVuetify3で動くように直します。
Slots(Vuetify2ではexpanded-item、Vuetify3ではexpanded-row)の外にtrが含まれなくなったので、複数行を簡単に出せるようになっています。
search/custom-filterとの組み合わせでも動くように直せました。filter使っている分、速度的には不利かもですが、過去に作ったロジックを生かす為に試してみました。
Vuetify3でリリースされたv-data-tableにアップデートする
expandを試す
Options API
元のコード: Vuetify2 -> expandを試す -> item全体をslotしていない場合
v-data-tableでも問題ないですが、v-data-table-serverに変更しています。
<template>
- <v-data-table
+ <v-data-table-server
:headers="headers"
:items="items"
- item-key="id"
+ :items-length="items.length"
:items-per-page="-1"
- hide-default-footer
- mobile-breakpoint="0"
+ density="comfortable"
+ hover
fixed-header
- :height="Math.max(200, $vuetify.breakpoint.height - 146) + 'px'"
+ :height="Math.max(200, $vuetify.display.height - 146) + 'px'"
- disable-sort
show-expand
>
<!-- 展開 -->
- <template #expanded-item="{ item }">
+ <template #expanded-row="{ item }">
- <!-- slot外でtrで囲まれているので、trで複数行出せない。hoverもしない -->
+ <tr>
<td class="px-1" />
<td class="pl-5 pr-1">
{{ item.name }}の内訳
</td>
<td class="text-end pl-1 pr-6">
500
</td>
<td class="text-end pl-1 pr-6">
700
</td>
+ </tr>
</template>
<!-- 値1 -->
<template #[`item.value1`]="{ item }">
{{ item.value1.toLocaleString() }}
</template>
<!-- 値2 -->
<template #[`item.value2`]="{ item }">
{{ item.value2.toLocaleString() }}
</template>
- </v-data-table>
+ </v-data-table-server>
</template>
<script>
export default {
data () {
return {
headers: [
- { value: 'data-table-expand', align: 'end', class: 'px-1', cellClass: 'px-1' },
+ { key: 'data-table-expand', align: 'end', headerProps: { class: 'px-1' }, cellProps: { class: 'px-1' } },
- { text: '名称', value: 'name', cellClass: 'font-weight-bold px-1', width: '75%' },
+ { title: '名称', key: 'name', sortable: false, cellProps: { class: 'font-weight-bold px-1' }, width: '75%' },
- { text: '値1', value: 'value1', align: 'end', class: 'pr-6', cellClass: 'pl-1 pr-6', width: 100 },
+ { title: '値1', key: 'value1', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, cellProps: { class: 'pl-1 pr-6' }, width: 100 },
- { text: '値2', value: 'value2', align: 'end', class: 'pr-6', cellClass: 'pl-1 pr-6', width: 100 }
+ { title: '値2', key: 'value2', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, cellProps: { class: 'pl-1 pr-6' }, width: 100 }
],
items: [
{ id: 10, name: '項目1', value1: 1500, value2: 2100 },
{ id: 20, name: '項目2', value1: 1700, value2: 2300 },
{ id: 30, name: '項目3', value1: 1900, value2: 2500 }
]
}
}
}
</script>
<style scoped>
.v-data-table >>> .v-data-table-footer {
display: none; /* NOTE: フッタを非表示にする為 */
}
</style>
Composition APIに書き換え
- <script>
+ <script setup lang="ts">
- export default {
- data () {
- return {
- headers: [
+ const headers: any = [
{ key: 'data-table-expand', align: 'end', headerProps: { class: 'px-1' }, cellProps: { class: 'px-1' } },
{ title: '名称', key: 'name', sortable: false, cellProps: { class: 'font-weight-bold px-1' }, width: '75%' },
{ title: '値1', key: 'value1', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, cellProps: { class: 'pl-1 pr-6' }, width: 100 },
{ title: '値2', key: 'value2', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, cellProps: { class: 'pl-1 pr-6' }, width: 100 }
- ],
+ ]
- items: [
+ const items = ref([
{ id: 10, name: '項目1', value1: 1500, value2: 2100 },
{ id: 20, name: '項目2', value1: 1700, value2: 2300 },
{ id: 30, name: '項目3', value1: 1900, value2: 2500 }
- ]
- }
- }
- }
+ ])
</script>
Composition APIの最終コード
<template>
<v-data-table-server
:headers="headers"
:items="items"
:items-length="items.length"
:items-per-page="-1"
density="comfortable"
hover
fixed-header
:height="Math.max(200, $vuetify.display.height - 146) + 'px'"
show-expand
>
<!-- 展開 -->
<template #expanded-row="{ item }">
<tr>
<td class="px-1" />
<td class="pl-5 pr-1">
{{ item.name }}の内訳
</td>
<td class="text-end pl-1 pr-6">
500
</td>
<td class="text-end pl-1 pr-6">
700
</td>
</tr>
</template>
<!-- 値1 -->
<template #[`item.value1`]="{ item }">
{{ item.value1.toLocaleString() }}
</template>
<!-- 値2 -->
<template #[`item.value2`]="{ item }">
{{ item.value2.toLocaleString() }}
</template>
</v-data-table-server>
</template>
<script setup lang="ts">
const headers: any = [
{ key: 'data-table-expand', align: 'end', headerProps: { class: 'px-1' }, cellProps: { class: 'px-1' } },
{ title: '名称', key: 'name', sortable: false, cellProps: { class: 'font-weight-bold px-1' }, width: '75%' },
{ title: '値1', key: 'value1', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, cellProps: { class: 'pl-1 pr-6' }, width: 100 },
{ title: '値2', key: 'value2', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, cellProps: { class: 'pl-1 pr-6' }, width: 100 }
]
const items = ref([
{ id: 10, name: '項目1', value1: 1500, value2: 2100 },
{ id: 20, name: '項目2', value1: 1700, value2: 2300 },
{ id: 30, name: '項目3', value1: 1900, value2: 2500 }
])
</script>
<style scoped>
.v-data-table >>> .v-data-table-footer {
display: none; /* NOTE: フッタを非表示にする為 */
}
</style>
search/custom-filterとの組み合わせる
Options API
元のコード: Vuetify2 -> search/custom-filterとの組み合わせる -> 共通化や操作性の向上
v-data-table-serverだと、custom-filterが動かないので、v-data-tableを使用しています。
Options APIでは、isExpanded(item)
-> isExpanded(ref(item))
と、refを付けないとexpandedにnullがセットされ正しく動かなくてハマりました。Vue3でもOptions APIは引き続き使用できますが、Composition APIに変えておいた方が良さそうですね。
<template>
<v-data-table
+ v-model:expanded="expanded"
:headers="headers"
:items="items"
- item-key="id"
:items-per-page="-1"
- hide-default-footer
- mobile-breakpoint="0"
+ density="comfortable"
+ hover
fixed-header
- :height="Math.max(200, $vuetify.breakpoint.height - 146) + 'px'"
+ :height="Math.max(200, $vuetify.display.height - 146) + 'px'"
- disable-sort
:search="searchCount.toString()"
:custom-filter="customFilter"
show-expand
- :expanded.sync="expanded"
- @item-expanded="itemExpanded"
>
- <template #item="{ index, item, expand, isExpanded }">
+ <template #item="{ index, item, isExpanded, toggleExpand }">
<tr :class="item.parent_id == null ? 'row_parent' : 'row_child'">
<!-- 行番号 // データに持たせなくても連番を表示できる。但し、展開すると番号が振り直される -->
<td class="text-end col_index px-2">
{{ (index + 1).toLocaleString() }}
</td>
- <!-- 展開 // v-data-table__expand-icon--activeを入れたり消したりするとアイコンが回転して向きが変わる -->
+ <!-- 展開 -->
<td class="px-1">
<v-icon
v-if="item.expand"
- :class="isExpanded ? 'v-data-table__expand-icon--active' : ''"
- @click="expand(!isExpanded)"
+ @click="showExpand(item, isExpanded, toggleExpand)"
>
- mdi-chevron-down
+ {{ isExpanded(ref(item)) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
</v-icon>
</td>
<!-- 名称 // エリアをクリックで展開 or 閉じる -->
<td class="px-1" @click="item.expand ? expand(!isExpanded) : void(0)">
<span :class="item.parent_id == null ? 'font-weight-bold' : 'pl-4'">
{{ item.name }}
</span>
</td>
<!-- 値1・2 -->
<td v-for="value of [item.value1, item.value2]" :key="value" class="text-end pl-1 pr-6">
<span :class="item.parent_id == null ? valueClass(value) : ''">
{{ value.toLocaleString() }}
</span>
</td>
</tr>
</template>
- <!-- これがないと展開した時にエラーになる。TypeError: this.$scopedSlots.expanded-item is not a function -->
- <template #expanded-item />
</v-data-table>
</template>
<script>
export default {
data () {
return {
searchCount: 0, // searchに値を入れると初回表示前にcustom-filterが呼び出される
expanded: [], // 展開した行のitemが追加、閉じたら対象のitemが削除される
headers: [
- { value: 'index', class: 'px-2' },
+ { key: 'index', sortable: false, headerProps: { class: 'px-2' } },
- { value: 'data-table-expand', class: 'px-1' },
+ { key: 'data-table-expand', headerProps: { class: 'px-1' } },
- { text: '名称', value: 'name', width: '75%' },
+ { title: '名称', key: 'name', sortable: false, width: '75%' },
- { text: '値1', value: 'value1', align: 'end', class: 'pr-6', width: 100 },
+ { title: '値1', key: 'value1', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, width: 100 },
- { text: '値2', value: 'value2', align: 'end', class: 'pr-6', width: 100 }
+ { title: '値2', key: 'value2', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, width: 100 }
],
items: [
// expand: 展開アイコンを表示するか否か, parent_id: 親のID(無い場合は親と判定)
{ id: 10, expand: true, name: '項目1', value1: 1500, value2: 2100 },
{ id: 11, parent_id: 10, name: '項目1の内訳1', value1: 500, value2: 700 },
{ id: 12, parent_id: 10, name: '項目1の内訳2', value1: 1000, value2: 1400 },
{ id: 20, expand: true, name: '項目2', value1: 1700, value2: 2300 },
{ id: 21, parent_id: 20, name: '項目2の内訳1', value1: 600, value2: 800 },
{ id: 22, parent_id: 20, name: '項目2の内訳2', value1: 1100, value2: 1500 },
{ id: 30, expand: false, name: '項目3', value1: 1900, value2: 2500 }
]
}
},
methods: {
// searchの値が変わると行毎に呼び出される。trueを返した行が表示される
- customFilter (value, search, item) {
+ customFilter (value, query, item) {
// eslint-disable-next-line no-console
- console.log('customFilter', value, search, item)
+ console.log('customFilter', value, query, item)
- if (item.parent_id == null) {
+ if (item.raw.parent_id == null) {
return true
} else {
- return this.expanded.some(expand => expand.id === item.parent_id)
+ return this.expanded.some(expand => expand.id === item.raw.parent_id)
}
},
- // 展開したり閉じたりすると呼び出される
+ // 展開したり閉じたりした時に呼び出す
- itemExpanded ({ item, value }) {
+ showExpand (item, isExpanded, toggleExpand) {
+ const value = !isExpanded(ref(item))
// eslint-disable-next-line no-console
- console.log('itemExpanded', item, value)
+ console.log('showExpand', item, value)
+ toggleExpand(ref(item))
this.searchCount += 1 // custom-filterが呼び出されるように値を変更
},
valueClass (value) {
- if (value <= 1700) {
- return 'red--text'
+ if (value <= 1700) { return 'text-red' }
- } else if (value <= 2100) {
- return 'yellow--text'
+ if (value <= 2100) { return 'text-yellow' }
- } else {
- return 'blue--text'
+ return 'text-blue'
- }
}
}
}
</script>
<style scoped>
+ .v-data-table >>> .v-data-table-footer {
+ display: none; /* NOTE: フッタを非表示にする為 */
+ }
/* ヘッダの背景色を変える */
- .v-data-table tr > th {
+ .v-data-table >>> tr > th {
background: dimgray !important;
}
/* 親項目の背景色を変える */
.v-data-table .row_parent {
background-color: gray;
}
/* 子項目の背景色を変える */
.v-data-table .row_child {
background-color: darkgray;
}
/* 子項目の行番号は親と同じ背景色に変える */
.v-data-table .row_child > .col_index {
background-color: gray;
}
/* hoverの色を変える */
.v-data-table tr:hover > td {
background-color: black !important;
}
</style>
Composition APIに書き換え
- {{ isExpanded(ref(item)) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
+ {{ isExpanded(item as any) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
- <script>
+ <script setup lang="ts">
- export default {
- data () {
- return {
- searchCount: 0, // searchに値を入れると初回表示前にcustom-filterが呼び出される
+ const searchCount = ref(0) // searchに値を入れると初回表示前にcustom-filterが呼び出される
- expanded: [], // 展開した行のitemが追加、閉じたら対象のitemが削除される
+ const expanded = ref([]) // 展開した行のitemが追加、閉じたら対象のitemが削除される
- headers: [
+ const headers: any = [
{ key: 'index', sortable: false, headerProps: { class: 'px-2' } },
{ key: 'data-table-expand', headerProps: { class: 'px-1' } },
{ title: '名称', key: 'name', sortable: false, width: '75%' },
{ title: '値1', key: 'value1', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, width: 100 },
{ title: '値2', key: 'value2', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, width: 100 }
- ],
+ ]
- items: [
+ const items = ref([
// expand: 展開アイコンを表示するか否か, parent_id: 親のID(無い場合は親と判定)
{ id: 10, expand: true, name: '項目1', value1: 1500, value2: 2100 },
{ id: 11, parent_id: 10, name: '項目1の内訳1', value1: 500, value2: 700 },
{ id: 12, parent_id: 10, name: '項目1の内訳2', value1: 1000, value2: 1400 },
{ id: 20, expand: true, name: '項目2', value1: 1700, value2: 2300 },
{ id: 21, parent_id: 20, name: '項目2の内訳1', value1: 600, value2: 800 },
{ id: 22, parent_id: 20, name: '項目2の内訳2', value1: 1100, value2: 1500 },
{ id: 30, expand: false, name: '項目3', value1: 1900, value2: 2500 }
- ]
- }
- },
+ ])
- methods: {
// searchの値が変わると行毎に呼び出される。trueを返した行が表示される
- customFilter (value, query, item) {
+ function customFilter (value: any, query: any, item: any) {
// eslint-disable-next-line no-console
console.log('customFilter', value, query, item)
if (item.raw.parent_id == null) {
return true
} else {
- return this.expanded.some(expand => expand.id === item.raw.parent_id)
+ return expanded.value.some((expand: any) => expand.id === item.raw.parent_id)
}
- },
+ }
// 展開したり閉じたりした時に呼び出す
- showExpand (item, isExpanded, toggleExpand) {
+ function showExpand (item: any, isExpanded: Function, toggleExpand: Function) {
const value = !isExpanded(ref(item))
// eslint-disable-next-line no-console
console.log('showExpand', item, value)
toggleExpand(ref(item))
- this.searchCount += 1 // custom-filterが呼び出されるように値を変更
+ searchCount.value += 1 // custom-filterが呼び出されるように値を変更
- },
+ }
- valueClass (value) {
+ const valueClass = computed(() => (value: number) => {
if (value <= 1700) { return 'text-red' }
if (value <= 2100) { return 'text-yellow' }
return 'text-blue'
- }
- }
- }
+ })
</script>
Composition APIの最終コード
<template>
<v-data-table
v-model:expanded="expanded"
:headers="headers"
:items="items"
:items-per-page="-1"
density="comfortable"
hover
fixed-header
:height="Math.max(200, $vuetify.display.height - 146) + 'px'"
:search="searchCount.toString()"
:custom-filter="customFilter"
show-expand
>
<template #item="{ index, item, isExpanded, toggleExpand }">
<tr :class="item.parent_id == null ? 'row_parent' : 'row_child'">
<!-- 行番号 // データに持たせなくても連番を表示できる。但し、展開すると番号が振り直される -->
<td class="text-end col_index px-2">
{{ (index + 1).toLocaleString() }}
</td>
<!-- 展開 -->
<td class="px-1">
<v-icon
v-if="item.expand"
@click="showExpand(item, isExpanded, toggleExpand)"
>
{{ isExpanded(item as any) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
</v-icon>
</td>
<!-- 名称 // エリアをクリックで展開 or 閉じる -->
<td class="px-1" @click="item.expand ? showExpand(item, isExpanded, toggleExpand) : void(0)">
<span :class="item.parent_id == null ? 'font-weight-bold' : 'pl-4'">
{{ item.name }}
</span>
</td>
<!-- 値1・2 -->
<td v-for="value of [item.value1, item.value2]" :key="value" class="text-end pl-1 pr-6">
<span :class="item.parent_id == null ? valueClass(value) : ''">
{{ value.toLocaleString() }}
</span>
</td>
</tr>
</template>
</v-data-table>
</template>
<script setup lang="ts">
const searchCount = ref(0) // searchに値を入れると初回表示前にcustom-filterが呼び出される
const expanded = ref<any>([]) // 展開した行のitemが追加、閉じたら対象のitemが削除される
const headers: any = [
{ key: 'index', sortable: false, headerProps: { class: 'px-2' } },
{ key: 'data-table-expand', headerProps: { class: 'px-1' } },
{ title: '名称', key: 'name', sortable: false, width: '75%' },
{ title: '値1', key: 'value1', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, width: 100 },
{ title: '値2', key: 'value2', sortable: false, align: 'end', headerProps: { class: 'pr-6' }, width: 100 }
]
const items = ref([
// expand: 展開アイコンを表示するか否か, parent_id: 親のID(無い場合は親と判定)
{ id: 10, expand: true, name: '項目1', value1: 1500, value2: 2100 },
{ id: 11, parent_id: 10, name: '項目1の内訳1', value1: 500, value2: 700 },
{ id: 12, parent_id: 10, name: '項目1の内訳2', value1: 1000, value2: 1400 },
{ id: 20, expand: true, name: '項目2', value1: 1700, value2: 2300 },
{ id: 21, parent_id: 20, name: '項目2の内訳1', value1: 600, value2: 800 },
{ id: 22, parent_id: 20, name: '項目2の内訳2', value1: 1100, value2: 1500 },
{ id: 30, expand: false, name: '項目3', value1: 1900, value2: 2500 }
])
// searchの値が変わると行毎に呼び出される。trueを返した行が表示される
function customFilter (value: any, query: any, item: any) {
// eslint-disable-next-line no-console
console.log('customFilter', value, query, item)
if (item.raw.parent_id == null) {
return true
} else {
return expanded.value.some((expand: any) => expand.id === item.raw.parent_id)
}
}
// 展開したり閉じたりした時に呼び出す
function showExpand (item: any, isExpanded: Function, toggleExpand: Function) {
const value = !isExpanded(ref(item))
// eslint-disable-next-line no-console
console.log('showExpand', item, value)
toggleExpand(ref(item))
searchCount.value += 1 // custom-filterが呼び出されるように値を変更
}
const valueClass = computed(() => (value: number) => {
if (value <= 1700) { return 'text-red' }
if (value <= 2100) { return 'text-yellow' }
return 'text-blue'
})
</script>
<style scoped>
.v-data-table >>> .v-data-table-footer {
display: none; /* NOTE: フッタを非表示にする為 */
}
/* ヘッダの背景色を変える */
.v-data-table >>> tr > th {
background: dimgray !important;
}
/* 親項目の背景色を変える */
.v-data-table .row_parent {
background-color: gray;
}
/* 子項目の背景色を変える */
.v-data-table .row_child {
background-color: darkgray;
}
/* 子項目の行番号は親と同じ背景色に変える */
.v-data-table .row_child > .col_index {
background-color: gray;
}
/* hoverの色を変える */
.v-data-table tr:hover > td {
background-color: black !important;
}
</style>