DataTables で一部のセルの値を動的に変更する方法（フィルター対応）に辿り着くまで

jQuery のプラグインである[DataTables](https://datatables.net/)を利用すると表にページ切り替えやフィルターなどの機能をあっという間に付加できます。

:heart: [Buttons](https://datatables.net/extensions/index) Extension を使えば、Excel や PDF に出力するためのボタンも簡単に付加できます。

大部分は、MITライセンスで利用できますが、編集機能を付け加える [Editor](https://editor.datatables.net/) は商用ライセンスです。Editor は非常に魅力的な機能を備えてますが、ちょっとした修正だけであれば、自前で機能を付け加えても良いでしょう。

# この記事を読むための前提となる知識など

- HTML の基本的なタグの知識
- CSS の基本的な知識
- jQuery の基本的な使い方
- CDN(Content Delivery Network)の概要

そのほか、使い過ぎの絵文字に耐えられる忍耐力…  :grin:

# 題材

今回は、以下のような表に DataTables を適用します。

|  連番  |    氏名           |      生年月日     |  状態  |
|:-----:|:-----------------:|:----------------:|:------:|
|  1    |     山田　太郎     |  昭和48年10月27日  |  不在  |
|  2    |     山田　花子     |  昭和52年 2月26日  |  在宅  |
|  3    |     山本　一郎     |  昭和52年 2月26日  |  在宅  |

この内、状態の列の「在宅」と「不在」はボタンにして、クリックすると相互に切り替わるようにします。なお、ボタンは状態に応じて背景色を青または赤に切り替えます。

 :sweat_smile: 正直、これくらいの人数であれば、DataTables を使うメリットはありませんが、話を簡単にするためにこれくらいにしておきます。大きい組織の状態管理とかに応用できるかもしれません。

一部のセルの値を動的に変える方法を検索してみましたが、ほとんどがサーバーから全データを JSON でもらってきて全更新する例でした。それでも十分早いみたいですが。

 :sweat_smile: ただ、複数人で使うことを考えるとそちらの方が常に最新の状態を確認できるのでよいかもしれません…。

# 必要なファイルを準備

jQuery や DataTables の使用に必要なファイルをダウンロードします。

CDN を利用できる場合は、CDN を利用した方が良いのですが、今回は、自身が使用するサーバーに設置して利用する想定で必要なファイルを準備します。（CDN を利用する方は、次の節に飛んでも大丈夫です。）

## DataTables をダウンロードして配置する

DataTables の [Download](https://datatables.net/download/index) ページにアクセスします。ダウンロードオプションは、初期値のままにします。

![download_option.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36738/3a7c14f3-7be7-f6ed-8488-2d67ca6cd72a.png)

ページの下部にある「Step 3. Pick a download method」で2番目にある「Download」タブを開きます。

![download_datatables.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36738/0dfa6a58-4642-cce8-c065-0785751c4cd4.png)

タブページの一番下にある ```Download files``` ボタンをクリックしてファイルをダウンロードします。

ダウンロードした DataTables.zip をすべて展開し、フォルダー DataTables-1.10.18 の中にある以下のフォルダーをこれから HTML を配置するフォルダーに配置します。

- css
- images
- js

## jQuery をダウンロードして配置する

jQuery の [Download](https://jquery.com/download/) ページにアクセスします。

「Download the compressed, production jQuery 3.4.1」というリンクを右クリックして、「名前を付けてリンク先を保存」（ブラウザにより異なる）し、前項で配置した js フォルダーに保存します。

# 必要なスタイルシートと JavaScript を読み込む記述を書く

必要なスタイルシートを読み込むための link タグとJavaScript を読み込むための script 要素を head 要素内に記述します。CDN を利用される方は、[Download](https://datatables.net/download/index)ページの下部にある例を確認してください。

```html
<head>
	<meta charset="UTF-8">
	<link rel="stylesheet" type="text/css" href="css/jquery.dataTables.min.css">
	<script type="text/javascript" charset="utf8" src="js/jquery-3.4.1.min.js"></script>
	<script type="text/javascript" charset="utf8" src="js/jquery.dataTables.min.js"></script>
```

# ボタンの色等を制御するスタイルシートを記述

いきなり脱線気味ですが…。

「在宅」の場合は、yes クラスとし、「不在」の場合は no クラスとします。

:sweat: もう少し、ましなクラス名を考えれば良かったのですが…

``head`` 要素内に ``style`` 要素を追加し、以下のスタイルシートを記述します。(

```css
button {
	color: white;
	font-weight: bold;
}

button.yes {
	background-color: blue;
}

button.no {
	background-color: red;
}
```

# DataTables を適用する table を HTML で記述

題材の行数であれば、すべて HTML で記述しても良いのですが、将来的にサーバーから受信したデータで書き換えることを考えて、空の table 要素を記述します。

```html
<body>
	<table id="table_id" class="cell-border compact">
	</table>
</body>
```

# 表に入れるデータの準備

以下、head 要素内に script 要素を追加し、その中に JavaScript を記述していきます。

以下のような二次元配列（配列の配列）を準備します。

```JavaScript
// データソース
var source = [
	['山田　太郎', '昭和48年10月27日', '不在', 0],
	['山田　花子', '昭和52年2月26日', '在宅', 1],
	['山本　一郎', '平成19年4月4日', '在宅', 2]
];
```

題材にあった連番は、データに含めず、行数を自動で表示するようにします。

各行の最後の列は、内部で管理するための値で表示はしない予定です。

# DataTables を適用するスクリプトを書く

基本的な DataTables の適用要領は、以下の通りです。table 要素がハードコーディングされていれば、これだけで DataTables 完了です。

```JavaScript
$(document).ready(function () {
	// DataTables 適用
	var table = $('#table_id').DataTable(/* 必要によりオプションを記述 */);
});
```

:notebook: table タグより前に記述してるので DOM の読み込みを待って実行するように ``$(document).ready`` イベント内に記述しています。table 要素の後にスクリプトを書く場合は、その部分を省略できます。(``thead`` と ``tbody`` を正しく使い分けないと IE **以外**では正常に動作しません。IE の Fazzy さは、有害だわ…。)

オプションは、JSON オブジェクトで与えます。data プロパティで表示するデータを指定し、columnDefs プロパティで列の定義できます。

:notebook: すべてのオプションは、公式ドキュメントの[Options](https://datatables.net/reference/option/)で確認してください。

```JavaScript
// DataTables 適用
var table = $('#table_id').DataTable({
	data: source,
	columnDefs: [
		{ "targets": 0, "name": "number", "title": "連番", "data": null},	// ←　1列名の定義
		{ "targets": 1, "name": "fullname", "title": "氏名", "data": 0},	// ←　2列名の定義
		{ "targets": 2, "name": "birthday", "title": "生年月日", "data": 1 },	// ←　3列名の定義
		{ "targets": 3, "name": "status", "title": "状態", "data": 2 },	// ←　4列名の定義
		{ "targets": 4, "name": "opt", "title": "オプション", "data": 3, "visible": false }	// ←　5列名の定義
	]
});
```

各列の定義にて指定しているプロパティは以下の通りです。

| プロパティ名 | 説明                                                                        |
| :----------: | :-------------------------------------------------------------------------- |
| targets      | 表示上の列番号（0オリジン）                                                 |
| name         | 調査中                                                                      |
| title        | 列ヘッダー部分に表示する項目名                                              |
| data         | 番号を指定するとデータソースの配列のインデックスを指定可能。その他は後述。  |
| visible      | 列を表示するか否か                                                          |

なお、[targets](https://datatables.net/reference/option/columnDefs.targets) は複数形になっているだけあって``[ 0, 2 ]``のように複数要素を指定することも可能です。

:sweat_smile: [name] (https://datatables.net/reference/option/columns.name) は列にアクセスるするための名前(?)と思われますが、ちゃんと調べてません。

## 連番が表示されるようにする

1列名の定義は、以下のように "data" に null を指定しています。

```javascript
{ "targets": 0, "name": "number", "title": "連番", "data": null},	// ←　1列名の定義
```

[data](https://datatables.net/reference/option/columns.data) に null が指定されている場合、他に [render](https://datatables.net/reference/option/columns.render) か [defaultContent](https://datatables.net/reference/option/columns.defaultContent) のいずれかのプロパティが設定されていないと以下のように行のデータソースがカンマ区切りで表示されてしまいます。

| 連番                                 | 氏名         | 生年月日           | 状態   |
| ------------------------------------ | ------------ | ------------------ | ------ |
| 山本　一郎,平成19年4月4日,在宅,2     | 山本　一郎   | 平成19年4月4日     | 在宅   |
| 山田　太郎,昭和48年10月27日,不在,0   | 山田　太郎   | 昭和48年10月27日   | 不在   |
| 山田　花子,昭和52年2月26日,在宅,1    | 山田　花子   | 昭和52年2月26日    | 不在   |

data には、関数も指定することができ、これにより計算等で値を設定することが可能です。指定する関数のプロトタイプは、次のとおりです。

```javascript
function data( row, type, set, meta )
``` 

それぞれの引数で受け取れるオブジェクトは以下の通りです。

| 引数     | 受け取れるオブジェクト                          |
| :------: | :-----------------------------                  |
| row      | 行のデータソース配列                            |
| type     | 値を設定する種別。(display、type、sort、filter) |
| set      | 調査中                                          |
| meta     | 列番号や行番号などのメタ情報                    |

連番を設定したいので meta.row プロパティから行番号（0オリジン）を取得して、+1 した値を return するように 以下のように無名関数を設定しました。

```javascript
{ "targets": 0, "width": "3em", "title": "連番", type: "num",
	"data": function (row, type, set, meta) {
		return Number(meta.row) + 1; 
	}
},
```

また、数値としてソートされるように ```type: "num"``` を指定しています。（この指定をしないとデータが 11 件以上存在した時に 1 の次が 11 になったりします。）

私の理解では、[columns.type](https://datatables.net/reference/option/columns.type) で説明されている以下の type が指定できると認識しています。

| type         | 説明                                              |
| :--:         | :---                                              |
| date         | 日付/時刻                                         |
| num          | 単純な数値                                        |
| num-fmt      | 通貨記号などが入った数字文字列                    |
| html-num     | HTML タグで囲まれた数値                           |
| html-num-fmt | HTML タグで囲まれた通貨記号などが入った数字文字列 |
| html         | HTML タグで囲まれた文字列                         |
| string       | 文字列                                            |


## 状態列がボタンで表示されるようにする

上記のソースのままだと「状態」の列が文字列のまま表示されてしまうので [render](https://datatables.net/reference/option/columns.render) イベントで表示時にボタンになるように３行目の定義を以下のように変更します。

```JavaScript
{
	"targets": 3,
	"name": "status",
	"title": "状態",
	"data": 2,
	"render": function (data, type, row, meta) {
		var tagClass = '';
		var tagText = '';

		switch (data) {
			case '在宅':
				tagText = '<button class="yes" onclick="btnClick(this)">'
					+ data + '</button>'
				break;
			case '不在':
				tagText = '<button class="no" onclick="btnClick(this)">'
					+ data + '</button>'
				break;
			default:
				tagText = data;
		}

		return type === "type" ? "html" : tagText;
	}
},
```

# ボタンを押した時の動作を実装（失敗編）

button 要素の onclick イベントに指定したハンドラ btnClick() を実装します。

button 要素の内容を確認し、innerText と class 属性を変更しています。（DOMを直接操作）

:sweat: 今回はサーバーとのやり取りは省略しています。

```javascript
function btnClick(button) {
	switch (button.innerText) {
		case '在宅':
			button.innerText = '不在';
			button.class = "no";
			break;
		case '不在':
			button.innerText = '在宅';
			button.class = "yes";
			break;
	}
}
```

実際に試してみると文字は切り替わるのですが、背景色が変わりません。インスペクタで見ても class 属性はちゃんと書き換えられているのですが…。

しかも検索用のテキストボックスに「在宅」とかいれても正しくフィルタリングされません。

# ボタンを押した時の動作を実装（成功編）

フィルタリング等を高速に行うために DOM とは別に内部データみたいなのを保有していてそれを書き換えないと正しく動作しないんだろうな、ということは予測できたのですが、なかなか、日本語の情報を見つけることができず、公式ドキュメントをいろいろ調べてみました。

結論から申し上げますと [row().data()](https://datatables.net/reference/api/row().data()) という API を利用しました。

:exclamation: 似ている API として、[rows().data()](https://datatables.net/reference/api/rows().data()) がありますが、これは読取専用です。

row(selector).data() で行のデータソースを配列で受け取ってから、配列を書き換えたうえで
row(selector).data(Array) で行のデータソースを修正し、[draw()](https://datatables.net/reference/api/draw()) で再描画しています。

:sweat: 今思えば、今回の場合は、[cell().data()](https://datatables.net/reference/api/cell().data()) が最適解だったな…。

```javascript
function btnClick(button) {
	const STATUS_COLUMN = 2;
	var prow = button.parentNode.parentNode;
	var rdata = $('#table_id').DataTable().row(prow).data();

	switch (rdata[STATUS_COLUMN]) {
		case '在宅':
			rdata[STATUS_COLUMN] = '不在';
			break;
		case '不在':
			rdata[STATUS_COLUMN] = '在宅';
			break;
	}
	$('#table_id').DataTable().row(prow).data(rdata).draw();
}
```

# とりあえずの全ソース

もっとスマートに書けたらいいなと思いつつ、全ソースを載せます。

```html:example.html
<!DOCTYPE html>
<html lang="ja">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<title>Datatable Example</title>
	<link rel="stylesheet" type="text/css" href="css/jquery.dataTables.min.css">
	<script type="text/javascript" charset="utf8" src="js/jquery-3.4.1.min.js"></script>
	<script type="text/javascript" charset="utf8" src="js/jquery.dataTables.min.js"></script>
	<script type="text/javascript">
function btnClick(button) {
	const STATUS_COLUMN = 2;
	var prow = button.parentNode.parentNode;
	var rdata = $('#table_id').DataTable().row(prow).data();

	switch (rdata[STATUS_COLUMN]) {
		case '在宅':
			rdata[STATUS_COLUMN] = '不在';
			break;
		case '不在':
			rdata[STATUS_COLUMN] = '在宅';
			break;
	}
	$('#table_id').DataTable().row(prow).data(rdata).draw();
}

		$(document).ready(function () {
			// データソース
			var source = [
				['山田　太郎', '昭和48年10月27日', '不在', 0],
				['山田　花子', '昭和52年2月26日', '在宅', 1],
				['山本　一郎', '平成19年4月4日', '在宅', 2]
			];

			// DataTables 適用
			var table = $('#table_id').DataTable({
				data: source,
				columnDefs: [
					{ "targets": 0, "width": "3em", "title": "連番", type: "num",
						"data": function (row, type, set, meta) {
							return Number(meta.row) + 1; 
						}
					},
					{ "targets": 1, "name": "fullname", "title": "氏名", "data": 0 },
					{ "targets": 2, "name": "birthday", "title": "生年月日", "data": 1 },
					{
						"targets": 3,
						"name": "status",
						"title": "状態",
						"data": 2,
						"render": function (data, type, row, meta) {
							var tagClass = '';
							var tagText = '';

							switch (data) {
								case '在宅':
									tagText = '<button class="yes" onclick="btnClick(this)">'
										+ data + '</button>'
									break;
								case '不在':
									tagText = '<button class="no" onclick="btnClick(this)">'
										+ data + '</button>'
									break;
								default:
									tagText = data;
							}

							return type === "type" ? "html" : tagText;
						}
					},
					{ "targets": 4, "name": "opt", "title": "オプション", "data": 3, "visible": false }
				]
			});

		});
	</script>
	<style>
button {
	color: white;
	font-weight: bold;
}

button.yes {
	background-color: blue;
}

button.no {
	background-color: red;
}
	</style>
</head>

<body>
	<table id="table_id" class="cell-border compact">
	</table>
</body>

</html>

```