Vuetifyのv-data-tableを使ってチェックリストを作ってみた

## やりたいこと
* 買い物リストなどチェックリストを入力する
* 項目をクリックするとチェック済みとなる
* チェック済みの項目は一括で削除できる
* データの取得/更新はAPIで行う

今回はVuetifyを使っていたので、v-data-tableを使って実現しました。
Vuetifyの環境構築については[こちらのページ](https://qiita.com/tToyoshima/items/a5da2fd2162fb3c28580)に記載しています。

## ソースコードの構成
src/componentにチェックリスト表示/入力用のコンポーネントを作ります。
実際にはsrc/views配下にチェックリスト用の親ページを作り複数のチェックリストを管理します。
親ページから各チェックリスト画面に遷移しますが、今回はコンポーネントの中身のみを説明します。

## チェックリストコンポーネント
```src/component/CheckListComponent.vue
<template>
  <div>
    <v-card>
      <v-card-title class="body-2"> リスト詳細 </v-card-title>
      <v-card-actions>
        <v-spacer />
        <v-btn text @click="newRegister"> 項目追加 </v-btn>
      </v-card-actions>
      <v-container class="py-0">
        <v-data-table
          class="pb-1"
          :headers="headers"
          :items="list"
          hide-default-header
          hide-default-footer
        >
          <template v-slot:item="{ item }">
            <tr>
              <template v-for="each in headers">
                <td
                  v-if="item.is_checked === 0"
                  width="286px"
                  :key="each.value"
                  class="text-left cursor-pointer body-1"
                  @click="changeCheck(item.detail_id, item.is_checked)"
                >
                  {{ item[each.value] }}
                </td>
                <td
                  v-else
                  width="286px"
                  :key="each.value"
                  class="text-left cursor-pointer body-1"
                  @click="changeCheck(item.detail_id, item.is_checked)"
                >
                  
{{ item[each.value] }}

                </td>
              </template>
            </tr>
          </template>
        </v-data-table>
      </v-container>
      <v-card-actions>
        <v-btn text @click="deleteChecked"> チェック済み削除 </v-btn>
        <v-spacer />
        <v-btn text @click="close"> 閉じる </v-btn>
      </v-card-actions>
    </v-card>
    <v-dialog
      v-if="isShowRegister"
      v-model="isShowRegister"
      width="310"
      persistent
    >
      <RegisterCheckListDialogComponent
        v-on:close="onRegisterClose"
        :listId="listId"
      ></RegisterCheckListDialogComponent>
    </v-dialog>
  </div>
</template>

<script>
import { mapActions } from "vuex";
import RegisterCheckListDialogComponent from "@/components/RegisterCheckListDialogComponent";

export default {
  name: "CheckListDialogComponent",
  components: {
    RegisterCheckListDialogComponent,
  },
  data() {
    return {
      headers: [{ text: "リスト", value: "detail_name", width: "100px" }],
      list: [],
      selectedItem: null,
      isShowRegister: false,
    };
  },
  props: {
    listId: null,
  },
  async created() {
    await this.getCheckListData();
  },
  methods: {
    ...mapActions("life-companion", [
      "getCheckList",
      "setCheckLists",
      "patchCheckOn",
      "patchCheckOff",
      "deleteCheckList",
    ]),
    newRegister() {
      this.isShowRegister = true;
    },
    async onRegisterClose() {
      this.isShowRegister = false;
      await this.getCheckListData();
    },
    async getCheckListData() {
      await this.getCheckList(this.listId)
        .then((response) => {
          this.list = response.data;
          this.setCheckLists(response.data);
        });
    },
    close() {
      this.$emit("close");
    },
   async deleteChecked() {
      await this.deleteCheckList(this.listId)
        .then(() => {
          this.getCheckListData();
        });
    },
    async changeCheck(id, flag) {
      if (flag === 0) {
        await this.patchCheckOn(id);
      } else {
        await this.patchCheckOff(id);
      }
      await this.getCheckListData();
    },
  },
};
</script>

<style scoped></style>

```

以下で細かく説明します。

### データテーブル
```vuejs
<v-data-table
  class="pb-1"
  :headers="headers"
  :items="list"
  hide-default-header
  hide-default-footer
>
```
`:headers`は列の要素です。
チェックリスト中身として表示する内容は`:items`として渡します。
チェックリストはヘッダが必要ないため、`hide-default-header`でヘッダを非表示としています。
`hide-default-footer`でフッタも非表示としています。

```vuejs
headers: [{ text: "リスト", value: "detail_name", width: "100px" }],
list: [],
```
ヘッダはあらかじめ`data()`に定義しておきます。listは宣言だけしておきます。

```
<template v-slot:item="{ item }">
  <tr>
    <template v-for="each in headers">
      <td
        v-if="item.is_checked === 0"
        width="286px"
        :key="each.value"
        class="text-left cursor-pointer body-1"
        @click="changeCheck(item.detail_id, item.is_checked)"
      >
        {{ item[each.value] }}
      </td>
      <td
        v-else
        width="286px"
        :key="each.value"
        class="text-left cursor-pointer body-1"
        @click="changeCheck(item.detail_id, item.is_checked)"
      >
        
{{ item[each.value] }}

      </td>
    </template>
  </tr>
</template>
```
上記は`v-slot`を使ってitemのレコード単位に見てます。
headersに定義した項目数分処理してますが、ここでは1項目のみです。
itemの`is_checked`が`0`の場合はそのまま`detail_name`が表示され、`is_checked`が`0`以外の場合は取り消し線が引かれた`detail_name`が表示されます。
`@click="changeCheck(item.detail_id, item.is_checked)"`はクリックされた際に呼ばれるメソッドを定義しています。

### チェックリストのデータ構造
```json
[
  {
    "detail_id": 2,          // 項目ID
    "list_id": 2,            // 親のID
    "detail_name": "猫砂",    // 項目名
    "is_check": 0            // チェックフラグ
  },
  ...
]
```
リストのデータ構造としては上記のようになっています。

### 初期表示
```vuejs
async created() {
  await this.getCheckListData();
},
```
```vuejs
async getCheckListData() {
  await this.getCheckList(this.listId)
    .then((response) => {
      this.list = response.data;
      this.setCheckLists(response.data);
    });
},
```
画面表示時にMapActionでストア経由のAPIでデータを取得し、ストアに保持しています。

### 登録用ダイアログ
```vuejs
<v-dialog
  v-if="isShowRegister"
  v-model="isShowRegister"
  width="310"
  persistent
>
  <RegisterCheckListDialogComponent
    v-on:close="onRegisterClose"
    :listId="listId"
  ></RegisterCheckListDialogComponent>
</v-dialog>
```
上記は登録用のダイアログです。
```vuejs
<v-btn text @click="newRegister"> 項目追加 </v-btn>
```
```vuejs
newRegister() {
  this.isShowRegister = true;
},
async onRegisterClose() {
  this.isShowRegister = false;
  await this.getCheckListData();
},
```
項目追加ボタンが押されたら表示されるようにしています。（ダイアログの説明は省略）

### イベント処理
```vuejs
async changeCheck(id, flag) {
  if (flag === 0) {
    await this.patchCheckOn(id);
  } else {
    await this.patchCheckOff(id);
  }
  await this.getCheckListData();
},
```
リストがクリックされたら呼ばれるメソッドです。
`is_checked`の状態に応じて、チェックフラグを更新するAPIをストア経由で呼び出します。（APIは省略）

```vuejs
<v-btn text @click="deleteChecked"> チェック済み削除 </v-btn>
```
```vuejs
async deleteChecked() {
   await this.deleteCheckList(this.listId)
     .then(() => {
       this.getCheckListData();
     });
 },
```
チェックした（取り消し線をつけた）項目をリストから一括削除するAPIを実行します。（APIは省略）

## 完成
下記のようなチェックリストが表示されます。（背景は親ページのリスト）
![スクリーンショット 2022-06-06 22.34.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529227/61007e2f-2c2b-f087-5f5c-a0aad01fb0dc.png)

項目をクリックすると取り消し線が引かれます。
![スクリーンショット 2022-06-06 22.34.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529227/eef2219c-c374-c0d8-668b-29cf5c7362dd.png)

「チェック済み削除」ボタンを押下することでチェック済みの項目が削除されます。
![スクリーンショット 2022-06-06 22.35.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529227/cd72ba13-d4e3-117d-0d20-be8ab445d71a.png)