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

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

作成した表をユーザー操作できるようにする。まずは手始めに「下に行を追加」ボタンをクリックすると行が追加されるようにする。


**▼完成形のイメージ**

①要素が選択されていない場合は下に追加

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/b577c4a5-2777-8940-d8ab-58916990cea6.png)|
|:--|

↓　1行追加

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/6be5c247-7744-031e-f1d7-a22e232737fe.png)|
|:--|


<br>
②要素が選択されている場合はその行の下に追加
※複数選択の場合は最初に選択した要素を対象とする。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/bbfbf4e9-02cd-1854-dde2-1fd4984a81cc.png)|
|:--|

↓　一番上の要素を軸に下に2行追加。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/ec533579-3e8b-1db7-d0d0-7aa63a9d9c48.png)|
|:--|

選択中のセルがシフトするようにする。



<br>
##考え方

1. クリックイベントを検知
2. 選択中の要素がある場合はその行番号を取得（ない場合は一番下に追加）
3. 選択した行とそれ以降で分割し、間に新しい行を追加してマージ
4. 選択中の要素の行番号を必要に応じて一つ下にずらす。



##作成方法

テーブルの作成方法については[こちら](https://qiita.com/yuta-38/items/93807929f83991045ee5)

以下では追加処理についてのみ記述。


##ボタンの作成
以下のようなボタンを作成。クリックイベントを設定する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/ac0cac8b-1338-ca0d-8e17-b24c97348f7a.png)


```vue:templateタグ内
      <button
        @click="addRowAfter"
      >下に行を追加
      </button>
```

`@click="addRowAfter"`
ボタンをクリックすると、addRowAfterメソッドが発火する。

```scss:styleの設定
<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: lightblue;
   padding: 5px 20px;
   color: white;
   border-radius: 50px;
}
</style>
```

<br>
##行追加メソッド

`addRowAfterメソッド`の中身を作成する。

```js:addRowAfter
addRowAfter(){
      //現在選択中のセルの行番号を取得（複数選択の場合は最初の行）
      let addRowIndex = this.rows.length
      
      if(this.currentCells[0] != null){
          addRowIndex = this.currentCells[0].xxxrowIndex
        }

      
      this.rows = [
        ...this.rows.slice(0, addRowIndex+1), //自分も含めるため＋１
        ...[this.createTr()],
        ...this.rows.slice(addRowIndex +1)
      ]

      //選択中の要素をずらす
      for (let currentCell of this.currentCells){
        if (currentCell.xxxrowIndex > addRowIndex){
          currentCell.xxxrowIndex += 1
        }
      } 
    },
```

<br>
###行の判定
まずは、どこに行を追加するかを判定するため、addRowIndexという変数を設ける。

```js:どこに行を追加するかを判定
      let addRowIndex = this.rows.length
      
      if(this.currentCells[0] != null){
          addRowIndex = this.currentCells[0].xxxrowIndex
        }
```

・`let addRowIndex = this.rows.length`
rowsには表の情報が入っている、中の要素は行ごとに入っているため、lengthで行の数を取得。

何も選択されていない時は、この数値を基準にすることで、表の一番下に行を追加していく。


```js:rowsの内容例
rows: [
        {
          "table_cells": [
            {"cell_type": "TD"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
        {
          "table_cells": [
            {"cell_type": "TD"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
      ]
```


<br>
**▼選択中の要素がある場合**
選択した要素の0番目の行番号を取得する。

```js:選択中の行があるか確認
      if(this.currentCells[0] != null){
          addRowIndex = this.currentCells[0].xxxrowIndex
        }
```

・`this.currentCells[0] != null`
currentCellsは現在選択中のセルの行番号とセル番号が入っている。

値がある（空でない）なら変数addRowIndexに行番号を代入する。


```js:currentCellsの内容例
currentCells : [
 { "xxxrowIndex": 1, "xxxcellIndex": 1 },
 { "xxxrowIndex": 0, "xxxcellIndex": 1 } 
]
```



<br>
###行の追加
sliceメソッドとスプレッド構文を利用して、選択した行の下に新たな行を追加する。


```js:行の追加処理
this.rows = [
        ...this.rows.slice(0, addRowIndex+1), //自分も含めるため＋１
        ...[this.createTr()],
        ...this.rows.slice(addRowIndex +1)
      ]
```

`createTr()`は、現在の表の状態に合わせて作成した行要素。このメソッドは別で定義している（後述）。


<br>
###選択中の要素をずらす
指定した行より下の行が選択中の場合、選択中のセルを下にズラす必要がある。


```js:選択中の要素をずらす
      for (let currentCell of this.currentCells){
        if (currentCell.xxxrowIndex > addRowIndex){
          currentCell.xxxrowIndex += 1
        }
      } 
```

もし、追加した行より下なら、各要素の行番号に１を足していく。




<br>
##追加するための行の作成
間に追加する行は現在の表セル数に合わせる必要がある。

クリックイベントが発生した時点で、表の状態から追加用の行要素を作成する。


```js:追加するための行を作成する
    createTr(){
      const maxCellNum = this.getMaxCellNum()
      let tds = [] //作成した行を格納する

      for(var i = 0; i < maxCellNum; i++){
        tds = [...tds, {cell_type:'TD'}]
      }

      return {
        table_cells: tds
      }
    },
```

・`const maxCellNum = this.getMaxCellNum()`
現在のセルの最大数をmaxCellNumに代入する。

最大のセル数を取得するgetMaxCellNumについては後述する。

追加用の行として変数tdsを用意し、最大のセル数分tdタグになる要素`{cell_type:'TD'}`を格納する。


```js:tdsの例(セル３つの場合)
        {
          "table_cells": [
            {"cell_type": "TD"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        }
```



<br>
##行内の要素（セル数）の最大値を取得
現在の表の最大のセル数を取得するメソッドを作成する。

指定した配列の要素を一つづつ取得し、最終的に一つの処理結果を返す`reduce`メソッドを使う。

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

```js:行内の要素（セル数）の最大値を取得する
    getMaxCellNum(){
      return this.rows.reduce((acc, tr) => {
        if (acc < tr.table_cells.length){
          return tr.table_cells.length
        }else{
          return acc
        }
      }, 0)
    }
```

reduceメソッドの詳細については[こちら](https://qiita.com/yuta-38/items/9c4487cd7882dc3d4bef)


以上の処理で狙った通りに行が追加できる。


<br>
##フルコード

```vue:
<template>
  <div>
      <button
        @click="addRowAfter"
      >下に行を追加
      </button>
    </p>

    <table>
      <template v-for="(tr, rowIndex) in rows">
        <tr :key="rowIndex">
            <td :key="cellIndex"
                :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"},
          ]
        },
      ]
    }
  },
  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
          }
        ]
      }
    },
    addRowAfter(){
      //現在選択中のセルの行番号を取得（複数選択の場合は最初の行）
      let addRowIndex = this.rows.length
      
      if(this.currentCells[0] != null){
          addRowIndex = this.currentCells[0].xxxrowIndex
        }

      
      this.rows = [
        ...this.rows.slice(0, addRowIndex+1), //自分も含めるため＋１
        ...[this.createTr()],
        ...this.rows.slice(addRowIndex +1)
      ]

      //選択中の要素をずらす
      for (let currentCell of this.currentCells){
        if (currentCell.xxxrowIndex > addRowIndex){
          currentCell.xxxrowIndex += 1
        }
      } 
    },
    //追加するための行を作成する
    createTr(){
      const maxCellNum = this.getMaxCellNum()
      let tds = [] //作成した行を格納する

      for(var i = 0; i < maxCellNum; i++){
        tds = [...tds, {cell_type:'TD'}]
      }

      return {
        table_cells: tds
      }
    },
    //行内の要素（セル数）の最大値を取得する（行作成用）
    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: lightblue;
   padding: 5px 20px;
   color: white;
   border-radius: 50px;
}
</style>
```


作成するセルの値に`( {{rowIndex}} , {{cellIndex}} )`を設定することで、セルの行列番号を表示することができる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/3aaac067-34b2-4cf1-663c-931bebdbd1fe.png)



<br>
##上に行を追加する。
下に行を追加したのと同じ流れで、上に行を追加する処理を追加する。

下に行を追加する処理と異なるのは３点。

1. 現在選択中のセルがない場合は、一番上に行を追加する。（下の場合は一番下に追加した）
2. 既存の行を分割する際に、基準となる行のより前と、基準となる行を含んだ後ろ部分で分断する
3. 選択中のセルをズラす際に、基準となる行が変わる


###1. 現在選択中のセルがない場合は、一番上に行を追加する。（下

```js:
      let addRowIndex = 0
      
      if(this.currentCells.length != 0){
          addRowIndex = this.currentCells[0].xxxrowIndex
        }
```

`let addRowIndex = 0`
基準とする行番号をデフォルトで0にする。



<br>
###2. 既存の行を分割する際に、基準となる行のより前と、基準となる行を含んだ後ろ部分で分断する

```js:
      this.rows = [
        ...this.rows.slice(0, addRowIndex), 
        ...[this.createTr()],
        ...this.rows.slice(addRowIndex)
      ]
```



<br>
###3. 選択中のセルをズラす際に、基準となる行が変わる

```js:
      //選択中の要素をずらす
      for (let currentCell of this.currentCells){
        if (currentCell.xxxrowIndex >= addRowIndex){
          currentCell.xxxrowIndex += 1
        }
      } 
    },
```

`if (currentCell.xxxrowIndex >= addRowIndex){}`
符号が、「>」から「>=」に変更となる。


<br>
###実装結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/49fce188-abb8-60c0-2ffb-4c80594e0e51.png)

↓ 上に2行を追加する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/9fbd569e-4cae-02e9-2f7c-3f9967f08020.png)



###フルコード

```vue:
<template>
  <div>
    <p>〜TmpAddRow.vue〜</p>
    <p>currentCells : {{currentCells}}</p>
    <p>
      <button
        @click="addRowAfter"
      >下に行を追加
      </button>
      <button
        @click="addRowBefore"
      >上に行を追加
      </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"},
          ]
        },
      ]
    }
  },
  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
          }
        ]
      }
    },
    addRowAfter(){
      //現在選択中のセルの行番号を取得（複数選択の場合は最初の行）
      let addRowIndex = this.rows.length
      
      if(this.currentCells.length != 0){
          addRowIndex = this.currentCells[0].xxxrowIndex
        }

      
      this.rows = [
        ...this.rows.slice(0, addRowIndex+1), //自分も含めるため＋１
        ...[this.createTr()],
        ...this.rows.slice(addRowIndex +1)
      ]

      //選択中の要素をずらす
      for (let currentCell of this.currentCells){
        if (currentCell.xxxrowIndex > addRowIndex){
          currentCell.xxxrowIndex += 1
        }
      } 
    },
    addRowBefore(){
      //現在選択中のセルの行番号を取得（複数選択の場合は最初の行）
      //選択なしの場合は一番上を基準とする
      let addRowIndex = 0
      
      if(this.currentCells.length != 0){
          addRowIndex = this.currentCells[0].xxxrowIndex
        }

      
      this.rows = [
        ...this.rows.slice(0, addRowIndex), 
        ...[this.createTr()],
        ...this.rows.slice(addRowIndex)
      ]

      //選択中の要素をずらす
      for (let currentCell of this.currentCells){
        if (currentCell.xxxrowIndex >= addRowIndex){
          currentCell.xxxrowIndex += 1
        }
      } 
    },
    //追加するための行を作成する
    createTr(){
      const maxCellNum = this.getMaxCellNum()
      let tds = [] //作成した行を格納する

      for(var i = 0; i < maxCellNum; i++){
        tds = [...tds, {cell_type:'TD'}]
      }

      return {
        table_cells: tds
      }
    },
    //行内の要素（セル数）の最大値を取得する（行作成用）
    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: lightblue;
   padding: 5px 20px;
   color: white;
   border-radius: 50px;
}
</style>
```

コードを眺めると複雑そうで引くけど、一つづつのパーツはシンプル。シンプルな物を重ねていくと思っていた以上に簡単にできる。


<br>
##（追記）行の追加処理をもっと簡単に書く方法

sliceメソッドとスプレッド構文で要素を追加する処理は、spliceメソッドを使うことでかなり簡単に書ける。


###spliceとは？
spliceとは配列への要素追加で何かと便利なメソッド。

指定した位置に追加するだけでなく、その追加位置以降の要素を削除することもできる。

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

<br>
**▼spliceでできること**

1. 指定した要素を追加する。
　┗　`arr.splice(挿入番号, 0, 挿入する要素)`
　┗ 削除する要素を0とすることで、要素追加のみに止める。

2. 指定した要素で置き換える。
　┗ `arr.splice(挿入番号, 1, 挿入する要素)`
　┗ 元々挿入した場所にあったセルを削除する

削除する要素数を増やせば、複数個の要素と置き換えることができる。


<br>
###1. 指定した要素を追加する

```js:
a = [1,2,3,4]

a.splice(2,0,"aaa")
console.log(a)

//出力
[1, 2, "aaa", 3, 4]
```

`a.splice(2,0,"aaa")`
配列aの配列番号2に、aaaという要素を追加。


<br>
###2. 指定した要素で置き換える

```js:
a = [1,2,3,4]

a.splice(2,1,"aaa")
console.log(a)

//出力
[1, 2, "aaa", 4]
```

`a.splice(2,1,"aaa")`
配列番号２にaaaを挿入し、後ろの要素を一つ削除。



<br>
###3. 指定した要素と複数の要素を置き換える

```js:
a = [1,2,3,4,5,6]

a.splice(1,4,"aaa")
console.log(a)

//出力
[1, "aaa", 6]
```

`a.splice(1,4,"aaa")`
配列番号１にaaaを挿入して、後ろの要素を４つ削除する。



##spliceで行追加処理をかく
以上のspliceを応用すると、行追加が簡単になる。

```js:spliceを使用
this.rows.splice(addRowIndex, 0, this.createTr())
```

かなりシンプルな記述になった。

<br>
**▼（参考）spliceを使わない場合（sliceとスプレッド構文**

```js:spliceを使わない場合（sliceとスプレッド構文）
      this.rows = [
        ...this.rows.slice(0, addRowIndex), 
        ...[this.createTr()],
        ...this.rows.slice(addRowIndex)
      ]
```