Vueではv-forとv-if, v-else-ifを組み合わせることで、自分でtableタグ(テーブル・表)を作りだすことができます。
ここでは、自分で作成した表の各要素を選択可能にする方法について考え方を実例を踏まえてまとめています。
最終的には以下のように選択した場合は枠を青色にし、選択を解除した場合は元の色に戻すテーブルができあがります。各セルの複数選択も可能にします。
選択状態の考え方
選択状態を判断する手順
セルを選択状態にするには、以下の4つの条件を満たすことが必要となります。
まとめると、セルをクリックした際に、クリックイベントで選択中のセルに選択中を表す属性があるかを判定し、属性があればそれを外し、なければ付与するという処理を行います。
セルの識別
どのセルがクリックされたかを判断するためには、各セルに固有の識別番号を振り分ける必要があります。
各セルは以下のように、左上の座標を(0, 0)として、(X, Y)という数値で識別するようにします。
▼配列番号によるセルの識別イメージ
0, 0 | 0, 1 | 0, 2 |
1, 0 | 1, 1 | 1, 2 |
2, 0 | 2, 1 | 2, 2 |
例えば、左上のセルをクリックした場合は、列番号0、行番号0のセルに属性を付与する操作となります。
使用する主な機能やメソッド
選択状態の判断のために使う主なVueの機能とJavaScriptのメソッドは以下になります。
クラス属性の付け外し
クラス属性にv-bind(省略形は「:」)を使うことで、状態に合わせクラス属性を付与したり外したりすることができます。
:class="{'付与するクラス名': 真偽値"}
真偽値は、booleanとなる変数でも条件式をいれても動きます。
クリックイベント
セルがクリックされたときに、属性有無の判断を行うためのクリックイベントを設定します。
@click="イベント名(引数)"
@clickにより、要素がクリックされると指定したイベントが発火します。
イベント処理の内容はmethodsオプションの中に記述します。
methods:{
イベント名(引数) {
処理
},
$event
Vueではイベントが発生したときに、どの要素でどんなイベントが発生したかなどの膨大な情報を格納している独自の変数「$event」があります。
@clickによりイベントが発火したときに、イベントが発生した画面上の位置や、イベントが発生した要素、その親要素の情報などがオブジェクト形式で入っています。
例えば以下のような情報があります。
今回は、「イベントが発生した要素(タグ)」の情報である「target」と「イベントが発生した要素の親要素」の「target.parentNode」を使用します。
findIndexメソッド
JavaScriptのメソッドである「findIndex」で要素のインデックス番号を取得します。
「findIndex」は指定した配列の要素をfor文で一つづつ取り出し、条件と一致した要素があればその配列番号を返すメソッドです。
arr.findIndex( ( 引数 ) => ( 条件式 ) )
sliceメソッド
JavaScriptのメソッドであるsliceを活用して、指定した値を非破壊で削除します。
slice自体は指定した範囲を指定して要素を抜き出すメソッドです。
arr.slice(開始番号,終了番号)
スプレッド構造
非破壊で配列の中に新たに要素を追加するためにJavaScriptのスプレッド構文を使います。
スプレッド構文とは、配列の前に「…」をつけることで、配列のカッコ[ ]を外す処理です。
例えば、配列同士を結合したいときに、それぞれの配列にスプレッド構文を付けて、カッコ[ ]で囲むと結合した新たな配列を作成できます。
a = [1,2]
b = [3,4]
console.log(...a)
//1 2
console.log([...a, ...b])
//[1,2,3,4]
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のエラー対策です。
・<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メソッド、スプレッド構文が鍵となります。