【Vue3+Vuetify3】データテーブル作成(Filter+Sort)

CSS

最近バージョンアップされたVue3とVuetify3を利用してデータテーブルを実装します。
Vuetify3で新規に盛り込まれたmulti-sort機能を利用して複数カラムのソートを実現しました。
また、グローバルフィルタで検索できるようにしています。(今回カラムフィルタは複雑になるので除外しました)

環境

名前バージョン
vuev3.x
vuetifyv3.4.0
materialdesigniconsv4.x

実装

Vueなのでコンポーネントでの実装になります。
・HTMLの実装

<div id="app">
  <v-card
          flat
          title="グローバルフィルタ(Vuetify3 + Vue3)"
          >
    <template v-slot:text>
      <v-text-field
                    v-model="search"
                    label="Search"
                    prepend-inner-icon="mdi-magnify"
                    single-line
                    variant="outlined"
                    hide-details
                    ></v-text-field>
    </template>

    <v-data-table
                  :headers="headers"
                  :items="desserts"
                  :search="search"
                  multi-sort
                  >
      
       <template v-slot:headers="{ columns, isSorted, getSortIcon, toggleSort }">
        <tr>
          <template v-for="column in columns" :key="column.key">
            <td>
              <span class="mr-2 cursor-pointer" @click="() => toggleSort(column)">
                {{ column.title }}
              </span>
              <template v-if="isSorted(column)">
                <v-icon :icon="getSortIcon(column)"></v-icon>
              </template>
              <v-icon v-if="column.removable" icon="$close" @click="() => remove(column.key)"></v-icon>
              
            </td>
          </template>
        </tr>
      </template>
    </v-data-table>
  </v-card>
</div>

・スクリプトの実装

<script type="module">
  const { createApp, computed, ref } = Vue;
  const { createVuetify } = Vuetify;

  const vuetify = createVuetify();
  const app = createApp({
    setup(){
      const filters = {
          name: '',
          calories: '',
          fat: '',
          carbs: '',
          protein: '',
          iron: '',
       };
        
      const desserts = [
        {
          name: 'Frozen Yogurt',
          calories: 159,
          fat: 6.0,
          carbs: 24,
          protein: 4.0,
          iron: '1%',
        },
        {
          name: 'Ice cream sandwich',
          calories: 237,
          fat: 9.0,
          carbs: 37,
          protein: 4.3,
          iron: '1%',
        },
        {
          name: 'Eclair',
          calories: 262,
          fat: 16.0,
          carbs: 23,
          protein: 6.0,
          iron: '7%',
        },
        {
          name: 'Eclair Dark',
          calories: 269,
          fat: 19.0,
          carbs: 29,
          protein: 7.0,
          iron: '9%',
        },
        {
          name: 'Eclair Light',
          calories: 190,
          fat: 9.0,
          carbs: 19,
          protein: 2.0,
          iron: '3%',
        },
        {
          name: 'Cupcake',
          calories: 305,
          fat: 3.7,
          carbs: 67,
          protein: 4.3,
          iron: '8%',
        },
        {
          name: 'Gingerbread',
          calories: 356,
          fat: 16.0,
          carbs: 49,
          protein: 3.9,
          iron: '16%',
        },
        {
          name: 'Jelly bean',
          calories: 375,
          fat: 0.0,
          carbs: 94,
          protein: 0.0,
          iron: '0%',
        },
        {
          name: 'Lollipop',
          calories: 392,
          fat: 0.2,
          carbs: 98,
          protein: 0,
          iron: '2%',
        },
        {
          name: 'Honeycomb',
          calories: 408,
          fat: 3.2,
          carbs: 87,
          protein: 6.5,
          iron: '45%',
        },
      ];

      const search = ref('');
      const headers = ref([
        {
          title: 'Dessert (100g serving)',
          align: 'start',
          key: 'name',
          removable: true
        },
        { title: 'Calories', key: 'calories', removable: true },
        { title: 'Fat (g)', key: 'fat', removable: true },
        { title: 'Carbs (g)', key: 'carbs', removable: true },
        { title: 'Protein (g)', key: 'protein', removable: true },
        { title: 'Iron (%)', key: 'iron', removable: true }
      ]);
        
      const remove = (key) => {
        headers.value = headers.value.filter(header => header.key !== key)
      };

      return {
        desserts,
        headers,
        search,
        filters,
        remove
      }
    }
  });
  
  app.use(vuetify).mount('#app');
</script>

本番で利用する場合はコンポーネントの特性を生かして、実データは親コンポーネントから受け取り(Propsして)、子コンポーネントは表示だけにした方が汎用的に使えそうですね。

今回はCodepenでも公開しましたので、実際の動作を確認してみて下さい。(リンク切れになった場合は上記コードを参考にして下さい)

See the Pen
Vuetify3 + Vue3 Data Table
by tamaya (@tamayalab)
on CodePen.

システム構成について考える

本記事で提示したようなVueでフィルタを掛ける場合は、全てのデータを取得してからでないとフィルタリングできない仕様になっています。
それだとクライアント・サーバの双方で処理負荷が掛かってしまうため、システムで実装する際は注意が必要です。
例えば下記4つのシステムがある場合を考えてみましょう。
 ①利用頻度が高いシステム
 ②利用頻度が低いシステム
 ③数万、数十万というレスポンスがある
 ④数百、数千程度のレスポンスがある

②、④であればVueのフィルタ実装でよいと思いますが、①、③のようなシステムの場合、フィルタ機能の強化(Pagenationによるページ単位での取得やInfinite scrollerでの問い合わせの細分化)などを利用してDB問い合わせ方法を考慮してあげる必要があります。
人手が余っているような余裕のあるPJであればいいのですが、どこも人手不足だと思いますので、設計する際はFilter設計についても考慮して開発を進めていければいいのかなと思います。

最後に

Web開発業界のライフサイクルはかなり激しいですね。
バージョンが変わると別言語を書いているような錯覚を覚えます。
理解している方もいると思いますが、Vue、Vue2、Vuetify、Vuetify2で作成したコードはVue3/Vuetify3では動作しません。(ある程度の互換性は持たせてほしいですね)
おそらく本記事で掲載したコードのようにVue3/Vuetify3で作成したコードはVue4/Vuetify4では利用できなくなると思います。
どこまでついていけるか分かりませんが、動作原理はあまり変化しないため、記述方法や新規・廃止・アップデート機能を整理して習得していくしかないのかなと思います。(大変だな。。。
以上です。最後までお付き合い頂きありがとうございました。