【Vue.js】Vuetifyのv-data-tableでitemsをcomputedで取り扱いやすくすることについて

# 概要
以下、Vue.jsのUIライブラリである[Vuetify](https://vuetifyjs.com/en/)の話です。
`v-data-table`に描画したいデータをサーバから取得した際、受け取ったデータそのままだと扱いにくいのでなんとかしたいケースがあったので、その時のメモです。

基本的にサーバの返却値を修正してもらうのが諸事情で面倒くさいのでクライアント側でなんとかしたい、という前提での話です。


[公式](https://vuetifyjs.com/en/components/data-tables#usage)の1番簡単な使い方を基準に考えます。
※items-per-pageとclassは今回の話に不要なので除去しています。

```vuejs:公式のUsageのソース
<template>
  <v-data-table
    :headers="headers"
    :items="desserts"
  ></v-data-table>
</template>
```

Ajaxで受け取ったデータでdessertsを随時上書きするイメージで、そのデータが扱いにくいという状況です。
ケースに応じて調査してでてきたもの、力技でなんとかした対応を紹介します。


# ケース1  チェックボックス(show-select)を使うとき、item-key用の一意のデータがない

```vuejs:コード1_show-selectを使う場合
<template>
  <v-data-table
    v-model="selected"
    :headers="headers"
    :items="desserts"
    item-key="name"
    show-select

  ></v-data-table>
</template>
<script>
export default {
  data () {
    return {
      selected: [],
      headers: [
        { text: '名前', value: 'name',  sortable: false },
        { text: '数値', value: 'value', sortable: false }
      ],
      desserts: [
        {
          name: 'data1',  value: 159,
          name: 'data1',  value: 200,
          name: 'data3',  value: 333,
        }
      ]
    }
  }
}
</script>
```

※data内で初期化してる部分がサーバからデータを受信したものとして見てください。

チェックボックスをテーブルに表示したい場合、`show-select`した上で`item-key`にitemsのいずれかのkeyをセットする必要があります。
このとき、keyには一意の値がくるものを指定する必要があります。
コード1では`item-key="name"`として一意のkeyとしてnameが指定されていますが、サーバから取得したdessertsはnameの値がdata1で被っています。
この状況は嬉しくありません。
`item-key`で指定したkeyの値が同一の場合、同じものと見なされてしまうためこの状況で数値が159か200の行のチェックボックスをクリックすると、クリックしなかったほうの行のチェックボックスまでチェックがついてしまうためです。
ちなみにこれは`single-select`がどちらの場合でも両方にチェックがつきます。

こうならないためには一意の値をもつkeyを指定したいわけですが、サーバから返却されたデータにDBのPK的な固有の識別番号がないケースでは対応できません。
というわけでこれの対応が以下です。

```vuejs:一意のデータを作るcomputed
<template>
  <v-data-table
    v-model="selected"
    :headers="headers"
    :items="indexedItems"
    item-key="id"
    show-select
  ></v-data-table>
</template>
<script>
//~途中省略~
  computed: {
    indexedItems () {
      return this.desserts.map((item, index) => ({
        id: index,
        ...item
      }))
    }
  }
}
</script>
```

mapを使ってindexをそのままidとして持たせ、そのidを`item-key`で指定する形ですね。
こちらは[公式のFeature Request](https://github.com/vuetifyjs/vuetify/issues/3897)からの知見です。スマートですね。


# ケース2  検索OKのテーブルで計算された値を使用したい
```vuejs:コード2_計算された値を使用する
<template>
  <v-data-table
    :headers="headers"
    :items="items"
    :search="search"
  ></v-data-table>
</template>
<script>
export default {
  data () {
    return {
      search: '',
      headers: [
        { text: '名前', value: 'name',  sortable: false },
        { text: '容量', value: 'capacity', sortable: false }
      ],
      items: [
        {
          name: 'data1',  value: 10485760,
          name: 'data2',  value: 20971520,
          name: 'data3',  value: 31457280,
        }
      ]
    }
  }
}
</script>
```

サーバから返却された容量の値がByte単位で、MBにしたいとします。
1024で2回割った値を使いたいわけなので、ケース１のようにcomputedを使ってあげればいいのはお分かりかと思います。

```vuejs:計算済みの値
<template>
  <v-text-field
    v-model="search"
  ></v-text-field>
  <v-data-table
    :headers="headers"
    :items="mbItems"
    :search="search"
  ></v-data-table>
</template>
<script>
//~途中省略~
  data () {
    return {
      search: '',
      headers: [
        { text: '名前', value: 'name',  sortable: false },
        { text: '容量', value: 'mb', sortable: false }
      ],
    }
  },
  computed: {
    mbItems () {
      return this.items.map((item) => ({
        mb: item.capacity / 1024 / 1024,
        ...item
      }))
    }
  }
}
</script>
```

computedは算出プロパティなんだからここでやるのが当然だろ、とお思いかもしれません。
ただ`v-data-table`では以下のようにすることができます。

```vuejs:v-slotを使用するパターン
<template>
  <v-text-field
    v-model="search"
  ></v-text-field>
  <v-data-table
    :headers="headers"
    :items="items"
    :search="search"
  >
    <template v-slot:item.capacity="{ item }">
      {{ item.capacity / 1024 / 1024}}
    </template> 
  </v-data-table>
</template>
```
`v-data-table`がもつ`v-slot`のうち`item.<name>`を使って容量のカラムだけtemplate側で計算するわけですね。
これでも表示は同じになるのですが、データの検索ができるテーブルでは`v-slot`での計算はよろしくありません。

なぜかと言うとtemlate側の`v-slot`で計算された値は、`v-data-table`がもつ`search`オプションでの検索にひっかかってくれないからです。
コード2の例だとMB単位で10,20,30のデータが表示されるわけですが、検索用の`v-text-field`に30といれても3行目がひっかかりません。
searchの値(30)を使ってitemsに対して検索をかけるため、
ちなみに上記の例だと10,20ではそれぞれ1行目,2行目がひっかかります。これは計算前の元からの値に含まれているためです。

このため、`v-slot`で描画時に計算するようにして10,20などで検索してみてOK!と軽く判断すると実はちゃんと検索がきいていない、ということになりかねません。
なので、検索可のテーブルに表示するデータを加工する場合はcomputedでやりましょう。（これが言いたかった）
~~私が最初`v-slot`でやっていて実は検索がきかなくて頭を抱えたのは内緒~~

# まとめ
- `show-select`を使うテーブルでは、mapを使ってidを一意のデータとすることができます
- データ検索ができるテーブルでは、計算済みデータを表示するときはcomputedでやりましょう