【Vue】自分で作成したテーブル(table・表)の各要素を選択可能にする方法と考え方を実例で解説|複数選択可能にする

vue-js-prograshi(プロぐらし)-kv Vue.js
記事内に広告が含まれていることがあります。

Vueではv-forとv-if, v-else-ifを組み合わせることで、自分でtableタグ(テーブル・表)を作りだすことができます。

ここでは、自分で作成した表の各要素を選択可能にする方法について考え方を実例を踏まえてまとめています。

最終的には以下のように選択した場合は枠を青色にし、選択を解除した場合は元の色に戻すテーブルができあがります。各セルの複数選択も可能にします。


合わせて読みたい

前提となるv-forを使ったテーブルの基本的な作り方や考え方については下記をご参考ください。

【Vue】v-forを使って自分でtable(表)を作る方法と考え方。tdタグとthタグの出し分け方を実例で解説


選択状態の考え方

選択状態を判断する手順

セルを選択状態にするには、以下の4つの条件を満たすことが必要となります。

セルの選択を判断する方法
  1. どのセルがクリックされたかを判断できるようにする。
  2. セルがクリックされたら、クリックイベントでそのセルに、選択中を表す属性を付ける。
  3. セルが再度クリックされたら、クリックイベントでそのセルに、選択中を表す属性を外す。
  4. クリックイベント実行時に選択中の属性がついているかいないかを判断する。

まとめると、セルをクリックした際に、クリックイベントで選択中のセルに選択中を表す属性があるかを判定し、属性があればそれを外し、なければ付与するという処理を行います。


セルの識別

どのセルがクリックされたかを判断するためには、各セルに固有の識別番号を振り分ける必要があります。

各セルは以下のように、左上の座標を(0, 0)として、(X, Y)という数値で識別するようにします。

▼配列番号によるセルの識別イメージ

0, 00, 10, 2
1, 01, 11, 2
2, 02, 12, 2

例えば、左上のセルをクリックした場合は、列番号0、行番号0のセルに属性を付与する操作となります。


使用する主な機能やメソッド

選択状態の判断のために使う主なVueの機能とJavaScriptのメソッドは以下になります。

クラス属性の付け外し

クラス属性にv-bind(省略形は「:」)を使うことで、状態に合わせクラス属性を付与したり外したりすることができます。

:class="{'付与するクラス名': 真偽値"}

真偽値は、booleanとなる変数でも条件式をいれても動きます。

合わせて読みたい

Vueを使ってv-bindでクラスを付与する方法はいくつかあります。実例や詳細については下記をご参考ください。

【Vue.js】v-bindを使ってclassやstyle属性を変更する方法を実例で解説


クリックイベント

セルがクリックされたときに、属性有無の判断を行うためのクリックイベントを設定します。

@click="イベント名(引数)"

@clickにより、要素がクリックされると指定したイベントが発火します。

イベント処理の内容はmethodsオプションの中に記述します。

  methods:{
    イベント名(引数) {
      処理
    },
合わせて読みたい

@clickやv-onなど、Vueでイベントを設定する方法やその詳細については下記をご参考ください。

【Vue.js】v-onや@アットマークの使い方を実例で解説


$event

Vueではイベントが発生したときに、どの要素でどんなイベントが発生したかなどの膨大な情報を格納している独自の変数「$event」があります。

@clickによりイベントが発火したときに、イベントが発生した画面上の位置や、イベントが発生した要素、その親要素の情報などがオブジェクト形式で入っています。

例えば以下のような情報があります。

$eventに含まれる主な情報の例
  • $event.target: イベントが発生した要素(タグ)
  • $event.target.parentNode: イベントが発生した要素の親要素
  • $event.type: 発生したイベント名 (clickなど)
  • $event.pageX: イベントが発生したx座標の値

今回は、「イベントが発生した要素(タグ)」の情報である「target」と「イベントが発生した要素の親要素」の「target.parentNode」を使用します。

合わせて読みたい


findIndexメソッド

JavaScriptのメソッドである「findIndex」で要素のインデックス番号を取得します。

「findIndex」は指定した配列の要素をfor文で一つづつ取り出し、条件と一致した要素があればその配列番号を返すメソッドです。

arr.findIndex( ( 引数 ) => ( 条件式 ) )
findIndexのポイントと注意点
  • 引数には対象の配列(arr)から一つづつ取り出した要素が入る。
  • 条件式に一致したら処理が終了し、対象のインデックス番号を返す。
  • 条件にマッチする要素が複数あっても、最初にマッチするものしか抜き出さない。
  • 指定した条件が見つからない場合は「-1」を返す。
合わせて読みたい

findIndexの実例や処理に改行がある場合の記述方法、function有り無しとの違いなど具体的な使い方については下記をご参考ください。

【JavaScript】findIndexメソッドとは何か?指定した条件にマッチ(満たす)する要素の配列番号を取得する方法を実例で解説


 

sliceメソッド

JavaScriptのメソッドであるsliceを活用して、指定した値を非破壊で削除します。

slice自体は指定した範囲を指定して要素を抜き出すメソッドです。

arr.slice(開始番号,終了番号)
sliceのポイントと注意点
  • 開始番号から、終了番号の手前までの要素を抜き出す。
  • 終了番号の要素は含まない。
  • 終了番号がない場合は、開始番号から最後の要素をまでを抜き出す。
  • 非破壊のため、処理結果を元の配列に置き換える場合は代入が必要。
非破壊とは?

非破壊とは、メソッドの対象となる元の要素自体はそのままの状態で処理することです。

対象の要素を他の場所でも参照していることがあるなど、壊れておかしくなることを防ぐため、非破壊で処理して、完全に処理が終わってから元の要素に代入するようにしています。


合わせて読みたい

sliceメソッドの使い方の実例や詳細については下記をご参考ください。

【JavaScript】sliceメソッドとは何か?配列の指定した要素を削除・追加する方法を実例で解説


スプレッド構造

非破壊で配列の中に新たに要素を追加するためにJavaScriptのスプレッド構文を使います。

スプレッド構文とは、配列の前に「…」をつけることで、配列のカッコ[ ]を外す処理です。

例えば、配列同士を結合したいときに、それぞれの配列にスプレッド構文を付けて、カッコ[ ]で囲むと結合した新たな配列を作成できます。

a = [1,2]
b = [3,4]

console.log(...a)
//1 2

console.log([...a, ...b])
//[1,2,3,4]
合わせて読みたい

スプレッド構文の使い方や注意点などの詳細については下記をご参考ください。

【JavaScript】ドット3つ「…」とは何か?スプレッド構文の使い方を実例で解説


1つのセルのみ選択可能にする

まずは、複数選択ではなく「1つのセルのみ選択可能」なテーブルを作成します。

セルをクリックするごとに選択中のセルを示す青枠が移動するようにします。

▼完成イメージ


templateの中身とmethodsの処理

各セルとなる、thタグに、「clickCell」というクリックイベントを設定し、「isActive」というメソッドで選択中か選択中でないかを判断して、「is-active」というクラスの値をtrueかfalseにします。

見出し以外のセル(tdタグ)も同様の処理となります。

      <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)">
              <br>
            </th>

v-forのエラー対策とrowIndexとcellIndex

v-forのエラー対策かつ、セルの行列番号を指定するために、各セルの行番号を「rowIndex」、列番号を「cellIndex」という変数に格納しています。

<tr :key="rowIndex">
v-forで取り出したtrタグのインデックス番号を変数rowIndexに格納し、v-bindでkeyの値に指定しています。v-forのエラー対策です。

合わせて読みたい

v-forを使うときの:keyの必要性については下記をご参考ください。

【Vue】v-forの:keyとは何か?|require-v-for-keyエラーの原因と対処法


<th :key="cellIndex"
v-forで取り出した各セルのインデックス番号を変数cellIndexに格納し、v-bindでkeyの値に指定しています。v-forのエラー対策です。


クリックイベント

@click="clickCell($event)"
セルの要素がクリックされたら、clickCellイベントを発動するクリックイベントの設定です。

イベントの詳細情報が入った変数$eventを引数で渡しています。

メソッドは以下のようになっています。

  methods:{
    clickCell(event){
      const cell = event.target
      const tr = event.target.parentNode

      if( this.currentCell.currentRowIndex == tr.rowIndex && this.currentCell.currentCellIndex == cell.cellIndex ){
        this.currentCell = {}
      }else{
        this.currentCell = {
          currentRowIndex: tr.rowIndex,
          currentCellIndex: cell.cellIndex
        }
      }
    },

const cell = event.target
$eventオブジェクトを引数eventとして渡し、targetプロパティを変数cellに代入する。

イベントが発生したセルの情報を変数セルに格納する。
このタグ中に格納されているcellIndexの取得目的。

const tr = event.target.parentNode
クリックした要素の親要素を変数trに代入する。
このタグ中に格納されているrowIndexの取得目的。

if( this.currentCell.currentRowIndex == tr.rowIndex && this.currentCell.currentCellIndex == cell.cellIndex
既に選択中のセルを選択した場合は、選択を解除するための条件分岐。

・currentRowIndex: tr.rowIndex
現在選択中のセルの情報を格納する変数currentCell内に、currentRowIndexを定義し、選択したセルの行番号を格納する。

currentCellはdataオブジェクトの中で以下のように定義しています。

  data(){
    return{
      currentCell:{},

・currentCellIndex: cell.cellIndex
現在選択中のセルの情報を格納する変数currentCell内に、currentCowIndexを定義し、選択したセルの列番号を格納する。

currentを付けているのは、trの中のrowIndexと区別するため。


選択中を表すクラスの付与 or 取り外し

:class="{'is-active': isActive(rowIndex, cellIndex)}"
{ クラス名: 条件式}というv-bindの指定方法です。

isActiveメソッドに、各セルの行と列のインデックス番号が入った変数「rowIndex」と「cellIndex」を渡し、処理を実行します。出力がtrueならクラスis-activeを付与し、falseならis-activeを外します。

メソッドは以下のようになっています。

  methods:{
  (省略)
    isActive(rowIndex, cellIndex){
      return this.currentCell.currentRowIndex == rowIndex && this.currentCell.currentCellIndex == cellIndex
    }
  }

this.currentCell.currentRowIndex == rowIndex
変数currentCell内に格納されているcurrentRowIndexの値と、for文で個別に取り出しているrowIndexの値が一致すれば、trueを返す。

this.currentCell.currentCellIndex == cellIndex
変数currentCell内に格納されているcurrentCellIndexの値と、for文で個別に取り出しているcellIndexの値が一致すれば、trueを返す。

上記の二つを「&&」でつないでいるので、2つの条件がtrueになる場合のみ、isActiveはreturnを返す。


スタイルの設定

thやtdタグ、また選択中を表すスタイルを以下のように設定しています。

<style lang="scss" scoped>
table{
  width: 80%;
  th,td{
    border: thin solid rgba(0, 0, 0, 0.12);
  }
  th{
    background: #ccc;
  }
  th, td{
    //選択状態
    &.is-active{
      border: 1px double #0098f7;
    }
  }
}
</style>

&.is-active
scssの表記で「&」は親のセレクタを表す。

「th.is-active」「td.is-active」と同じ。


ブラウザの表示

デフォルトでは以下のように何も選択されていません。

 ↓ 左上の座標(0, 0)のセルをクリックすると選択中になります。

 ↓ 座標(1, 1)をクリックすると選択中のセルが移動します。

 ↓ 同じセルをもう一度クリックすると選択中が解除されます。


(参考).vue全体のコード

参考に.vueファイルの全体のコードを記載しておきます。

<template>
  <div>
    <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)">
              <br>
            </th>

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

  </div>
</template>

<script>
export default {
  data(){
    return{
      currentCell:{},
      rows: [
        {
          "table_cells": [
            {"cell_type": "TH"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
        {
          "table_cells": [
            {"cell_type": "TH"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
        {
          "table_cells": [
            {"cell_type": "TH"},
            {"cell_type": "TD"},
            {"cell_type": "TD"},
          ]
        },
      ]
    }
  },
  methods:{
    clickCell(event){
      const cell = event.target
      const tr = event.target.parentNode

      if( this.currentCell.currentRowIndex == tr.rowIndex && this.currentCell.currentCellIndex == cell.cellIndex ){
        this.currentCell = {}
      }else{
        this.currentCell = {
          currentRowIndex: tr.rowIndex,
          currentCellIndex: cell.cellIndex
        }
      }
    },
    isActive(rowIndex, cellIndex){
      return this.currentCell.currentRowIndex == rowIndex && this.currentCell.currentCellIndex == cellIndex
    }
  }
}
</script>

<style lang="scss" scoped>
table{
  width: 80%;
  th,td{
    border: thin solid rgba(0, 0, 0, 0.12);
  }
  th{
    background: #ccc;
  }
  th, td{
    //選択状態
    &.is-active{
      border: 1px double #0098f7;
    }
  }
}
</style>

・現在選択中のセルの行列番号を格納する変数 currentCell を設ける。

・クリックイベントが発生したら、そのセル行列番号をcurrentCellにそれぞれ格納する。

・currentCellの行列番号と現在選択中の行列番号が一致すればtrueを返す。


複数選択を可能にする

先ほどは選択可能な要素は1つのみだったでしたが、複数選択する場合の例を紹介します。

▼完成イメージ


セルを1つだけ選択可能にする場合は、現在選択中のセルを格納する変数に1つの要素のみを入れました。

今回は複数のセル情報を格納します。このため、配列currentCellsを用意して、値を {} から [] に変更します。

  data(){
    return{
      currentCells:[],


class属性の判定|isActiveメソッドの中身

選択中かどうかを判断し、クラス属性is-activeを付与するかしないかを決めるisActiveメソッドの中身が以下のようになります。

  methods:{
    //isActiveの判定
    //currentCellsの中にあればtrueにする
    //指定した行列番号の要素がある=数値が-1以外ならtrueにする。
    isActive(rowIndex, cellIndex){
      return this.currentCells.findIndex((elem) =>
        elem.currentRowIndex == rowIndex && elem.currentCellIndex == cellIndex
        ) > -1
    },

変数currentCellsに指定したrowIndexとcellIndexに該当する要素がないか判定し、要素が含まれている場合はtrueを、ない場合はfalseを返します。

判定にはfindIndexメソッドを使います。

クリックメソッドが発生するたびに、v-forで各セルに対してisActiveメソッドが走り、選択中であればfindIndexの処理結果がに「-1」以外が返ります。

このため「findIndexの処理 >1」を戻り値(return)にすると、対象のセルが、選択中の中に含まれている場合はtrue、いない場合はfalseが変える処理となります。


クリックイベントの中身|clickCellメソッド

セルをクリックしたときに発動するclickCellメソッドの中身は以下のようになります。

methods:{

  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.currentRowIndex == tr.rowIndex && elem.currentCellIndex == cell.cellIndex 
        )

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

      } else{
        this.currentCells = [
          ...this.currentCells,
          {
            currentRowIndex: tr.rowIndex,
            currentCellIndex: cell.cellIndex
          }
        ]
      }
    },
  }


選択中かどうかで条件分岐

選択中かどうかで条件分岐させるため、if文の条件式でisActiveを使います。

if(this.isActive(tr.rowIndex, cell.cellIndex)


現在選択中の場合

現在選択中、すなわちisActiveの処理結果がtrueになる場合は、currentCellsからそのセルの情報を削除します。

まずは、findIndexメソッドを使って、クリックした要素の行列番号に該当する要素の、配列番号を取得します。

取得した配列番号を「rmIndex」という変数に格納します。

        //選択中の配列の何番目の要素かを求める
        const rmIndex = this.currentCells.findIndex((elem)=>
          elem.currentRowIndex == tr.rowIndex && elem.currentCellIndex == cell.cellIndex 
        )



次に、sliceメソッドとスプレッド構文を使って取得した要素を削除します。

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



現在選択中でない場合

現在選択中でない、すなわちisActiveの処理結果がfalseになる場合は、currentCellsにそのセルの情報を追加します。

スプレッド構文を使って非破壊で追加処理を行い、処理完了後に元の配列(currentCells)に代入します。

      else{
        this.currentCells = [
          ...this.currentCells,
          {
            currentRowIndex: tr.rowIndex,
            currentCellIndex: cell.cellIndex
          }
        ]
      }

以上の設定で複数が可能となります。


currentCellsの内容

例えば、以下のように3つのセルが選択されているとします。

現在選択中のセルを格納するcurrentCellsは次のうになります。

[ 
  { "currentRowIndex": 0, "currentCellIndex": 0 },
  { "currentRowIndex": 1, "currentCellIndex": 1 },
  { "currentRowIndex": 2, "currentCellIndex": 2 }
]


ブラウザの表示

デフォルト状態では以下のようになっています。

 ↓ (0, 0)と (1, 1)、(2, 2)のセルをクリック

複数選択することが可能です。 

 ↓再度(1, 1)のセルをクリック

選択中のセルをクリックすると、選択中が外れます。


(参考).vue全体のコード

参考に.vueファイルの全体のコードを記載しておきます。

<template>
  <div>
    <p>〜TmpTrTd.vue〜</p>
    <p>{{currentCells}}</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)">
              <br>
            </th>

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

  </div>
</template>

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

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

      } else{
        this.currentCells = [
          ...this.currentCells,
          {
            currentRowIndex: tr.rowIndex,
            currentCellIndex: cell.cellIndex
          }
        ]
      }
    },
  }
}
</script>

<style lang="scss" scoped>
table{
  width: 80%;
  th,td{
    border: thin solid rgba(0, 0, 0, 0.12);
  }
  th{
    background: #ccc;
  }
  th, td{
    //選択状態
    &.is-active{
      border: 1px double #0098f7;
    }
  }
}
</style>


findIndexメソッド、sliceメソッド、スプレッド構文が鍵となります。

タイトルとURLをコピーしました