【Vue.js】ライブラリを使わずにツリー型の展開可能テーブルを作成する

## 概要

Vue.jsで、クリックで展開すると子要素を表示できるテーブルを作成する方法をまとめました。

イメージ
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3655873/61050935-711f-0ed4-5be8-40ddbd472114.png)
　↓
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3655873/74befbbb-17f1-4801-041f-703589d7e48f.png)
## 経緯
イメージのようなツリー構造のテーブルを作成しようと思い、[Vuetifyのv-data-table](https://v2.vuetifyjs.com/ja/api/v-data-table/)のexpandを検討したのですが、「複数の子要素を挿入する」、「クリックで子要素の情報をもとに処理を実行する」、「trやtdにCSSを適用する」などの細かい設定が難しいため、1から専用のテーブルを作成いたしました。
## 環境
vue2.6.11

## テーブルの作成
### 作成したファイル
- Table.vue
- TreeTable.vue

構成
```
components
 ├ molecules
 │     └─Table.vue
 └ organisms
       └─TreeTable.vue
```


※スタイルの記述は処理に関連するもの以外は省略しています。
### Table.vue
```vuejs:Table.vue
<template>
  <table class="table">
    <thead>
      <slot name="header" />
    </thead>
    <tbody>
      <slot name="body" />
    </tbody>
  </table>
</template>

<script>
export default {
  name: 'Table',
}
</script>

```

tableタグ、theadタグ、tbodyタグなどをtable.vueで作成し、TreeTable.vue内で使用しています。

### TreeTable.vue
```vuejs:TreeTable.vue

<template>
  <div>
        <Table>
          <template #header>
            <template>
                <tr>
                    <th v-for="tableColumn in tableColumns"
                        :key="tableColumn.id">
                         {{tableColumn.value}}
                   </th>

                </tr>
            </template>  
          </template>
          <template #body>
                <template>
                    <template v-for="tableData in tableDataList" >
                        <tr  :key="tableData.name" @click="detailClick(tableData)" >
                            <template v-for="(tableColumn,tableColumnKey) in tableColumns">
                                <td :key="tableColumnKey.name" >
                                  <span v-show="tableData.button" v-if="tableColumn.name==='button'">
                                    <label>
                                      <input class="checkboxOpen" type="checkbox" @click="detailClick(tableData)">
                                      <span>
                                          <img src="ボタン画像" class="upIcon" :class="{ transform: tableData.show }"/>
                                      </span>
                                    </label>
                                  </span>
                                  <span v-if="tableColumn.name !== 'button'" >
                                    {{tableData[tableColumn.name]}}
                                  </span>
                                </td>
                            </template>
                        </tr>


                        <tr  v-for="(child,chKey) in tableData.childData" :key="chKey.name" v-show="tableData.show">
                            <template>
                                <template v-for="(tableColumn,tableColumnKey) in tableColumns" >
                                    <td :key="tableColumnKey">
                                        <span >{{child[tableColumn.name]}}</span>
                                    </td>
                                </template>
                            </template>
                        </tr>
                    </template>
                </template>
          </template>
        </Table>
      </div>
</template>
<script>
import Table from "@/components/molecules/Table"


export default {
    name: "TreeTable",
    data() {
        return{
          transform: false,
          tableColumns:[
                {id:1, name:'button', value:"展開"},
                {id:2, name:'column1', value:"ID"},
                {id:3, name:'column2', value:"プロジェクト名"},
                {id:4, name:'column3', value:"担当者"},
                {id:5, name:'column4', value:"工程名"},
                {id:6, name:'column5', value:"進捗状況"},
                {id:7, name:'column6', value:"作業開始日"},
                {id:8, name:'column7', value:"作業終了予定日"},
          ],
          tableDataList:[
                {id:1,
                button:true,
                show:false,
                column1: '1', 
                column2: 'プロジェクトＡ', 
                column3: '', 
                column4: '', 
                column5: '73.33%', 
                column6: '', 
                column7: '', 
                childData:[{id:1, column3:"田中",column4:"工程1", column5: '80.00%', column6: '2023/12/1', column7: '2023/12/31'}
                          ,{id:2, column3:"鈴木",column4:"工程2", column5: '70.00%', column6: '2023/12/1', column7: '2023/12/9'}
                          ,{id:3, column3:"山田",column4:"工程3", column5: '70.00%', column6: '2023/12/3', column7: '2023/12/8'}
                    ]
                },
                {id:2,
                button:true,
                show:false,
                column1: '2', 
                column2: 'プロジェクトＢ', 
                column3: '', 
                column4: '', 
                column5: '70.00%', 
                column6: '', 
                column7: '', 
                childData:[{id:1, column3:"山本",column4:"工程4", column5: '50.00%', column6: '2023/12/6', column7: '2023/12/10'}
                          ,{id:2, column3:"伊藤",column4:"工程5", column5: '90.00%', column6: '2023/12/4', column7: '2023/12/9'}
                    ]
                },
                {id:3,
                button:true,
                show:false,
                column1: '3', 
                column2: 'プロジェクトＢ', 
                column3: '', 
                column4: '', 
                column5: '70.00%', 
                column6: '', 
                column7: '', 
                childData:[{id:1, column3:"佐々木",column4:"工程6", column5: '20.00%', column6: '2023/12/2', column7: '2023/12/27'}
                          ,{id:2, column3:"中村",column4:"工程7", column5: '80.00%', column6: '2023/12/1', column7: '2023/12/25'}
                    ]
                },

            ],
        }
    },
    components: {
        Table,

     
    },
    methods:{
       detailClick(tableData){
        tableData.show = !tableData.show
        this.transformCD = !this.transformCD;
    }

    },
}
</script>
<style scoped>

.checkboxOpen{
    display:none
}

.upIcon{
    height: 18px;
    display: flex;
    justify-content: flex-end;
    text-align: left;
}

.upIcon.transform{
    height: 18px;
    display: flex;
    justify-content: flex-end;
    text-align: left;
    transform:scale(1,-1);
}



</style>
```

全体をTableタグで囲い、その中にツリー構造を作成しています。

**データの形式**
tableColumnsにカラムのデータ、tableDataListにテーブルのデータを格納し、tableDataList要素内のchildDataに子要素のデータを格納しています。
今回の例では子要素にcolumn1(ID),column2(プロジェクト名)は入れていませんが、データを持たせることは可能です。

**ヘッダー部分**
カラム名をv-forを使用して画面上に表示させています。

**ボディ部分**
同じくv-forでテーブルのデータを表示しています。
２つあるうちの１つ目のtrタグには親要素を表示し、データの「button」がtrueの時に展開用のボタンの画像を表示させています。
（画像はCSSで反転させています）

ボディ部分２つ目のtrタグには子要素が表示されます。親データにそれぞれ格納されている「show」をもとにv-showで子要素の開閉が行われます。

**画面**
![プレゼンテーション1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3655873/a21624a5-6c4c-2b20-45d8-63a9d9d95cbd.gif)

## 困ったこと
Vue2で、v-forを複数使用してテーブルの子要素を作成しようとするとkeyが重複する問題がありました。

``Duplicate keys detected``

このテーブルではtemplateタグでv-forを使用しているのですが、Vue2ではtemplateタグにkeyを設定することができないためtemplate内のタグでkeyに重複が発生し、
「使用する分には問題ないが、コンソールでエラーが出続ける」という状態に長く陥りました。
最初はkeyを``tableColumnKey-${tableColumnKey}``のように設定してエラーを回避していましたが、エラーが発生している箇所のkey設定を変更することで解決できました。
今回はVue2でkeyに重複がないようにして回避しましたが、Vue3ではtemplateタグにもkeyが設定できるようになっているなど仕様の変更があるようなので、エラーが出ることなく省略した形で実現できそうです。

## よかったこと
親要素のデータと子要素のデータを１から作成したため、簡単に細かい設定を変更することができます。
例えば２つ目のtrタグのみに＠clickやidを設定することで「親要素のみ、または子要素のみをクリック可能にして別ページに飛べるようにする」、「子要素のみデザインを変更する」などが実装可能です。

## まとめ
vue.jsでツリー型のテーブルの作成方法についてまとめました。
参考にしていただければ幸いです。