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>

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です