【Vue】テーブル操作エキスパートへの道。ボタンクリックで列を追加する方法

ボタンクリックで表に列を追加する方法について。


これまで、

・[v-forを使ったテーブルの作成](https://qiita.com/yuta-38/items/88ae6036e95f70d7d708)
・[表で複数選択を可能にする方法](https://qiita.com/yuta-38/items/93807929f83991045ee5)
・[ボタンクリックで行を追加する方法](https://qiita.com/yuta-38/items/d0f646c876b0cdabe0e7)

について確認したので、次のステップとして列を追加する方法を確認する。

まずは前への列追加から。


##やりたいこと

・ボタンをクリックすると前に列を追加する。
・選択している場合は、列追加後に選択中のセルが後ろにズレる


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/98a5367b-1860-ed65-4417-35ad092d3753.png)

↓ 前に２列追加

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/96791f0d-2d55-25d7-a8ac-71cc01151942.png)



##考え方

1. ボタンクリックで列を追加するイベントが発生
2. セルが選択されていない場合は先頭に列を追加
    3. セルが選択されている場合はその前に列を追加（複数選択の場合は一番最初に選択したセル）
3. 行をループ処理で抜き出し指定した位置にセル要素を追加
4. 選択中のセルをズラす



<br>
##1. ボタンクリックで列を追加するイベントが発生
イベントを設定する

```js:
      <button
        @click="addColumnBefore"
      >前に列を追加
      </button>
```

`@click="addColumnBefore"`
クリックイベントで、addColumnBeforeイベントを呼び出す


##列追加メソッド
クリックイベントで呼び出したaddColumnBeforeの設定を作成する。

```js:
    addColumnBefore(){
      //選択中のセル番号を取得（複数選択の場合は最初に選択したセル）
      //未選択の場合は0（一番先頭のセル）にする。

      let addCellIndex = 0

      if(this.currentCells.length != 0){
        addCellIndex = this.currentCells[0].xxxcellIndex
      }

      //セルの内容は簡単に変更できるように初期値(initCell)を別途設けておく
      this.rows.map((tr, index)=>{
        tr.table_cells.splice(addCellIndex, 0, this.initCell)
        }
      )

      //選択中のセルを移動する
      this.currentCells.map((tr, index)=>{
        if(tr.xxxcellIndex >= addCellIndex){
          tr.xxxcellIndex += 1
        }
      })
    }
  }
}
```


<br>
###2. セルが選択されていない場合は先頭に列を追加

```js:
      let addCellIndex = 0

      if(this.currentCells.length != 0){
        addCellIndex = this.currentCells[0].xxxcellIndex
      }
```

列追加の基準となるセル番号を addCellIndex に格納する。

・`this.currentCells.length != 0`
選択中の行列番号を格納しているcurrentCellsが空でない場合にtrueとなる。


・`addCellIndex = this.currentCells[0].xxxcellIndex`
選択したセルの列番号を代入する。



<br>
##3. 行をループ処理で抜き出し指定した位置にセル要素を追加
mapメソッドを活用する。mapメソッドは指定した配列の要素を一つづつ抜き出し、かく要素に対して処理を実行し配列を返す。

`arr.map((要素用の変数, インデックス番号用の変数)=>{処理})`
　┗ インデックス番号用の変数は省略可能

```js:
this.rows.map((tr, index)=>{
        tr.table_cells.splice(addCellIndex, 0, this.initCell)
        })
```

各行の指定した場所に列を追加する。追加にはspliceメソッドを使う。

・`arr.splice(挿入する場所, 削除する要素数, 挿入する要素)`

一つ一つの行要素は下記のようになっている。`{"cell_type": "TD"}`が１つのセルになる。

```js:行要素
        {
          "table_cells": [
            {"cell_type": "TD"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
```

・`this.initCell`
追加するセルはセルのデフォルト設定を変更する場合を考慮して変数で指定する。

変更をリアルタイムに反映させるため、computedプロパティで関数処理の結果として変数を出力する。

```js:computed_initCell
  computed:{
    initCell(){
      return {"cell_type": "TD"}
    },
  },
```

>※注
・メソッドの中でcomputedで設定した変数を呼び出す場合カッコは不要。
・メソッドの中でメソッドを呼び出す場合はカッコが必要。




<br>
##4. 選択中のセルをズラす
列挿入に合わせて選択中のセルをズラす。

こちらもmapメソッドを使用する。
現在選択中のセルを格納しているcurrentCellsから要素を一つづつ取り出し、列番号(xxxcellIndex)が挿入した基準位置より後ろなら、列番号を一つズラす。

```js:
this.currentCells.map((tr, index)=>{
        if(tr.xxxcellIndex >= addCellIndex){
          tr.xxxcellIndex += 1
        }
      })
```


以上で、作成完了。


<br>
##前に行追加のfullコード

```vue:fullコード
<template>
  <div>
    <p>〜TmpAddRow.vue〜</p>
    <p>currentCells : {{currentCells}}</p>
    <p>
      <button
        @click="addColumnBefore"
      >前に列を追加
      </button>
    </p>

    <table>
      <template v-for="(tr, rowIndex) in rows">
        <tr :key="rowIndex">
          <template v-for="(cell, cellIndex) in tr.table_cells">
            <th :key="cellIndex" 
                v-if="cell.cell_type == 'TH'"
                :class="{'is-active': isActive(rowIndex, cellIndex)}"
                @click="clickCell($event)">
              ( {{rowIndex}} , {{cellIndex}} )
            </th>
          
            <td :key="cellIndex" 
                v-else-if="cell.cell_type == 'TD'"
                :class="{'is-active': isActive(rowIndex, cellIndex)}"
                @click="clickCell($event)">
              ( {{rowIndex}} , {{cellIndex}} )
            </td>
          </template>
        </tr>
      </template>
    </table>

  </div>
</template>

<script>
export default {
  data(){
    return{
      currentCells:[],
      rows: [
        {
          "table_cells": [
            {"cell_type": "TD"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
        {
          "table_cells": [
            {"cell_type": "TD"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
                {
          "table_cells": [
            {"cell_type": "TD"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
      ]
    }
  },
  computed:{
    initCell(){
      return {"cell_type": "TD"}
    },
  },
  methods:{
    //isActiveの判定
    //currentCellsの中にあればtrueにする
    //指定した行列番号の要素がある＝数値が-1以外ならtrueにする。
    isActive(rowIndex, cellIndex){
      return this.currentCells.findIndex((elem) =>
        elem.xxxrowIndex == rowIndex && elem.xxxcellIndex == cellIndex
        ) > -1
    },

    clickCell(event){
      //クリックされたセルの情報
      const cell = event.target
      const tr = event.target.parentNode

      //クリックされたセルが既に選択されている場合は、配列から削除する
      if(this.isActive(tr.rowIndex, cell.cellIndex)){
        
        //選択中の配列の何番目の要素かを求める
        const rmIndex = this.currentCells.findIndex((elem)=>
          elem.xxxrowIndex == tr.rowIndex && elem.xxxcellIndex == cell.cellIndex 
        )

        //選択した要素を選択中の配列から削除する
        this.currentCells = [
          ...this.currentCells.slice(0, rmIndex),
          ...this.currentCells.slice(rmIndex + 1)
        ]

      } else{
        this.currentCells = [
          ...this.currentCells,
          {
            xxxrowIndex: tr.rowIndex,
            xxxcellIndex: cell.cellIndex
          }
        ]
      }
    },
    addColumnBefore(){
      //選択中のセル番号を取得（複数選択の場合は最初に選択したセル）
      //未選択の場合は0（一番先頭のセル）にする。

      let addCellIndex = 0

      if(this.currentCells.length != 0){
        addCellIndex = this.currentCells[0].xxxcellIndex
      }

      //セルの内容は簡単に変更できるように初期値(initCell)を別途設けておく
      this.rows.map((tr, index)=>{
        tr.table_cells.splice(addCellIndex, 0, this.initCell)
        }
      )

      //選択中のセルを移動する
      this.currentCells.map((tr, index)=>{
        if(tr.xxxcellIndex >= addCellIndex){
          tr.xxxcellIndex += 1
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>
table{
  width: 80%;
  th,td{
    border: thin solid rgba(0, 0, 0, 0.12);
    text-align: center;
    color: gray;
  }
  th{
    background: #ccc;
  }
  th, td{
    //選択状態
    &.is-active{
      border: 1px double #0098f7;
    }
  }
}
button{ 
   background: lightcoral;
   padding: 5px 20px;
   color: white;
   border-radius: 50px;
}
</style>
```



<br>
##後ろに列を追加する
### 完成イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/55274647-4532-6d11-9749-35ea980f5c7a.png)

↓ 後ろに２列追加

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/6606cf97-359d-eb7d-86e0-d94f71977907.png)


###考え方
基本的には前に列を追加するのと同じ。変更点は下記３つ。

1. セルが選択されていない場合は現在の行の中で最大のセル数を基準位置とする
2. 追加セルの挿入位置を基準位置＋１にする
3. 列挿入後の選択中のセル位置をシフトする


```js:後ろにセルを追加する処理
addColumnAfter(){
      //選択中のセル番号を取得（複数選択の場合は最初に選択したセル）
      //未選択の場合は最後のセルにする（最大のセル数）

      let addCellIndex = this.getMaxCellNum()

      if(this.currentCells.length != 0){
        addCellIndex = this.currentCells[0].xxxcellIndex
      }

      //セルの内容は簡単に変更できるように初期値(initCell)を別途設けておく
      this.rows.map((tr, index)=>{
        tr.table_cells.splice(addCellIndex + 1, 0, this.initCell)
        }
      )

      //選択中のセルを移動する
      this.currentCells.map((tr, index)=>{
        if(tr.xxxcellIndex > addCellIndex){
          tr.xxxcellIndex += 1
        }
      })
    },
    //行内の要素（セル数）の最大値を取得する（行作成用）
    getMaxCellNum(){
      return this.rows.reduce((acc, tr) => {
        if (acc < tr.table_cells.length){
          return tr.table_cells.length
        }else{
          return acc
        }
      }, 0)
    }
```



###1. セルが選択されていない場合は現在の行の中で最大のセル数を基準位置とする

```js:
let addCellIndex = this.getMaxCellNum()
```

getMaxCellNumメソッドでセルの最大数を取得する。

```js:getMaxCellNumメソッド
    getMaxCellNum(){
      return this.rows.reduce((acc, tr) => {
        if (acc < tr.table_cells.length){
          return tr.table_cells.length
        }else{
          return acc
        }
      }, 0)
    }
```

reduceメソッドでセルの最大値を取得する。

・`arr.reduce((accumulater, 要素)=>{処理}, 初期値)`

reduceメソッドは指定した配列の要素を一つづつ抜き出し、処理を実行した後の値で、同じ処理を繰り返し、一意の値を返すメソッド。

<br>
###2. 追加セルの挿入位置を基準位置＋１にする

```js:
      this.rows.map((tr, index)=>{
        tr.table_cells.splice(addCellIndex + 1, 0, this.initCell)
        }
      )
```

spliceで要素の追加位置を＋１する。


<br>
###3. 列挿入後の選択中のセル位置をシフトする

```js:
      this.currentCells.map((tr, index)=>{
        if(tr.xxxcellIndex > addCellIndex){
          tr.xxxcellIndex += 1
        }
      })
```

列番号をズラす要素の判定式を、`>=`から`>`にする。


<br>
###列を前後に追加するフルコード

```vue:
<template>
  <div>
    <p>〜TmpAddRow.vue〜</p>
    <p>currentCells : {{currentCells}}</p>
    <p>
      <button
        @click="addColumnBefore"
      >前に列を追加
      </button>
            <button
        @click="addColumnAfter"
      >後ろに列を追加
      </button>
    </p>

    <table>
      <template v-for="(tr, rowIndex) in rows">
        <tr :key="rowIndex">
          <template v-for="(cell, cellIndex) in tr.table_cells">
            <th :key="cellIndex" 
                v-if="cell.cell_type == 'TH'"
                :class="{'is-active': isActive(rowIndex, cellIndex)}"
                @click="clickCell($event)">
              ( {{rowIndex}} , {{cellIndex}} )
            </th>
          
            <td :key="cellIndex" 
                v-else-if="cell.cell_type == 'TD'"
                :class="{'is-active': isActive(rowIndex, cellIndex)}"
                @click="clickCell($event)">
              ( {{rowIndex}} , {{cellIndex}} )
            </td>
          </template>
        </tr>
      </template>
    </table>

  </div>
</template>

<script>
export default {
  data(){
    return{
      currentCells:[],
      rows: [
        {
          "table_cells": [
            {"cell_type": "TD"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
        {
          "table_cells": [
            {"cell_type": "TD"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
                {
          "table_cells": [
            {"cell_type": "TD"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
      ]
    }
  },
  computed:{
    initCell(){
      return {"cell_type": "TD"}
    },
  },
  methods:{
    //isActiveの判定
    //currentCellsの中にあればtrueにする
    //指定した行列番号の要素がある＝数値が-1以外ならtrueにする。
    isActive(rowIndex, cellIndex){
      return this.currentCells.findIndex((elem) =>
        elem.xxxrowIndex == rowIndex && elem.xxxcellIndex == cellIndex
        ) > -1
    },

    clickCell(event){
      //クリックされたセルの情報
      const cell = event.target
      const tr = event.target.parentNode

      //クリックされたセルが既に選択されている場合は、配列から削除する
      if(this.isActive(tr.rowIndex, cell.cellIndex)){
        
        //選択中の配列の何番目の要素かを求める
        const rmIndex = this.currentCells.findIndex((elem)=>
          elem.xxxrowIndex == tr.rowIndex && elem.xxxcellIndex == cell.cellIndex 
        )

        //選択した要素を選択中の配列から削除する
        this.currentCells = [
          ...this.currentCells.slice(0, rmIndex),
          ...this.currentCells.slice(rmIndex + 1)
        ]

      } else{
        this.currentCells = [
          ...this.currentCells,
          {
            xxxrowIndex: tr.rowIndex,
            xxxcellIndex: cell.cellIndex
          }
        ]
      }
    },
    addColumnBefore(){
      //選択中のセル番号を取得（複数選択の場合は最初に選択したセル）
      //未選択の場合は0（一番先頭のセル）にする。

      let addCellIndex = 0

      if(this.currentCells.length != 0){
        addCellIndex = this.currentCells[0].xxxcellIndex
      }

      //セルの内容は簡単に変更できるように初期値(initCell)を別途設けておく
      this.rows.map((tr, index)=>{
        tr.table_cells.splice(addCellIndex, 0, this.initCell)
        }
      )

      //選択中のセルを移動する
      this.currentCells.map((tr, index)=>{
        if(tr.xxxcellIndex >= addCellIndex){
          tr.xxxcellIndex += 1
        }
      })
    },
    addColumnAfter(){
      //選択中のセル番号を取得（複数選択の場合は最初に選択したセル）
      //未選択の場合は最後のセルにする（最大のセル数）

      let addCellIndex = this.getMaxCellNum()

      if(this.currentCells.length != 0){
        addCellIndex = this.currentCells[0].xxxcellIndex
      }

      //セルの内容は簡単に変更できるように初期値(initCell)を別途設けておく
      this.rows.map((tr, index)=>{
        tr.table_cells.splice(addCellIndex + 1, 0, this.initCell)
        }
      )

      //選択中のセルを移動する
      this.currentCells.map((tr, index)=>{
        if(tr.xxxcellIndex > addCellIndex){
          tr.xxxcellIndex += 1
        }
      })
    },
    //行内の要素（セル数）の最大値を取得する（行作成用）
    getMaxCellNum(){
      return this.rows.reduce((acc, tr) => {
        if (acc < tr.table_cells.length){
          return tr.table_cells.length
        }else{
          return acc
        }
      }, 0)
    }
  }
}
</script>

<style lang="scss" scoped>
table{
  width: 80%;
  th,td{
    border: thin solid rgba(0, 0, 0, 0.12);
    text-align: center;
    color: gray;
  }
  th{
    background: #ccc;
  }
  th, td{
    //選択状態
    &.is-active{
      border: 1px double #0098f7;
    }
  }
}
button{ 
   background: lightcoral;
   padding: 5px 20px;
   color: white;
   border-radius: 50px;
}
</style>
```

以上。処理を一つ一つ見ていくことが大切。

コード全体を見て、divide and conquer(分割統治法)の考え方の大切さを改めて実感。