【jquery+Datatables】行削除と各要素の値チェックに対応した簡単なテーブルを作る


フロントエンドエンジニアではない人が作った簡単な画面です。
だけど、体感的にこういう画面が使いたいときは結構ある気はする（すでにライブラリもあるかもしれないが）。

# 作ったもの

サーバサイドからajax経由でデータを受け取る

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281173/2dba7e59-db8f-c01f-9b3d-aaa0c4af2167.png)

名前に日本語などの変な文字列が入っていたり、数式に変な文字(画像の場合は大かっこ[])が入っていると、validation errorを発生させ、テキストフィールドに色を付けて、エラーメッセージをセルの近くに出す

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281173/69995b38-011d-5c11-b25e-f10f34159a04.png)

IDの付近を押すと「削除」のメニューが近くに出てくる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281173/b23db5f8-5f47-b3d4-4a29-ff605b2d280a.png)

「削除」を押すと行が消える

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281173/a5c86d90-2819-6819-5e87-7991c92774e1.png)

※追加については、次の記事で書く予定（若干コードが変わるため）
※削除後イベントとしてajaxを送る必要がある（今回はフロントエンド側メインなので、ajaxでDELETEリクエストを入れる部分は考えない）


# 解説

各種ポイントだけをピックアップ
なお、ASP.NET MVCを使用しているので、jsのプログラムとHTMLのプログラムはIndex.cshtmlにすべて同梱しているものとする

※基本的にはjqueryを使った経験は薄いので、色々ご指摘いただけると助かります。

## 開発環境

再現性のため、使用したライブラリは全部並べて書く

|役割|ライブラリ、フレームワーク|バージョン|
|---|---|---|
||ASP.NET MVC| Core 5|
||jquery|v3.5.1|
|UIライブラリ|jqueryui|v1.12.1|
|バリデーションチェック|jquery validation plugin|v1.17.0|
|画面整形用CSS|bootstrap 4| v4.3.1|
|データテーブルUI|DataTables| v1.10.21|



## ポイント①：menuなどは、DataTablesの添え字に依存する値を突っ込むためにカスタムデータを使う

まずはHTMLでの注意。

```html
<!--  ポイント① : data-menuindexとしメニューのクリックした行を確保できるようにする -->
<div id="contextmenu" class="text-left" style="display: none;" data-menuindex="0">
    <table class="table table-hover table-info table-sm">
        <tr onClick="pressMenu('remove')">
            <td>削除</td>
        </tr>
    </table>
</div>
```

メニューは別途HTMLに外部で定義して、それを右クリックしたときにマウスポインタの近くに来るようにしている。
このとき、カスタムデータなどで、DataTablesのクリック位置を覚えておけるようにしておきたい。

今回はたまたまメニューを使っているが、他の機能でもこういうのを使いたいときがあるので注意する

## ポイント② : 行の識別子としてmeta.rowを使用する

DataTablesの定義に注意をしよう
例えば、idは次のようになっている

```js
{
    data: "id",
    render: function(data, type, instance, meta) {
        /* ポイント② : 行の識別子としてmeta.rowを使用する */
        return '<label name="id_' + meta.row + '" 
                 class="js-text-input" data-row="' + meta.row + '">' + data + '</label>';
    }
},
```

ajaxでサーバサイドとフロントエンドでやりとりするときは"data"を使うのだが、
"render"を入れることでレンダリングを行うことが出来る。

このとき行番号に対応する値は（私が探した限り）、**meta**(第４引数)しかなかったので、これを使っている。
引数が長くなるし、データのアクセス権的には少し危うい部分もあるが、こうしてしまえば画面呼び出し時の行を識別子として割り当てることが出来る。

注意として、行の削除は容易だが、**任意行の追加を機能として取り入れる場合は、現在表示されている表の行の位置≠meta.rowの値**となることに気を付けなくてはいけない。この対処法については別の機会で扱うとして、フロントエンドでこうしたスマートな機能を入れる場合には一工夫がいるのだ。

## ポイント③：validatorをinitCompleteの中に入れて、動的にvalidation制約を追加する

一番大事なポイント。

通常、validationをするときはRules制約を使ったコード、例えば

```js
$('form').validate({
    rules: {
        "rowName": {
            required: false,
            isAlphabet: ''
        }
    }
});
```

と書くことが多い。これは簡単なのだが、一つ問題がある。
**rulesを「要素の数だけ動的に追加する」機能を必要とする**のだ。

従って、このrulesを連想配列で要素の数だけ突っ込む処理が必要なのだが、**DataTablesのセル要素を並べる作業は非同期で行われるので**、validateを実行するタイミングではDataTablesの中身がまだロードされていない可能性が高い。validateを使うやり方では無理がある。

これを解決するため、**DataTablesのinitCompleteを使って、**セルロードが完全に行われてからvalidation ruleを決めるようにした。

```js
initComplete: function(settings, ajax_json) {
    // もしDataTablesの描画が終わったら、
    // テキストボックスの色を変えたり
    // validationルールを加えたりする.
    for (i = 0; i < ajax_json.length; i++) {
        /* ポイント③ : 
           validatorをinitCompleteの中に入れて、 
           動的にvalidation制約を追加する
        */
        $('input[name=rowName_' + i + ']').rules("add", {
                isAlphabet: true
        });

        $('input[name=expression_' + i + ']').rules("add", {
                isValidExpression: true
        });

        // メニュー右クリック
        $('label[name=id_' + i + ']')[0].oncontextmenu = function(e) {
            $('#contextmenu').css({ 'position': 'absolute', 'left': e.pageX, 'top': e.pageY,
                                    'display': 'block' });
            // contextmenuの行の添え字を登録するのを忘れずに
            $('#contextmenu').data['rowindex'] = this.dataset['row'];

            // return falseとすることで、
            // ブラウザの右クリックを排除できる
            return false;
        };
    }
}
```

第２引数であるajax_jsonには、ajaxで受け取ったjsonデータが含まれるので、これを各タグの識別子と対応付けると良いと思う。
initCompleteに関する解説（英語）はこちら。

https://datatables.net/reference/option/initComplete

validationを追加するときは、`rules("add", ...)`を使用する。
動的な制約指定の場合に使用する。

```js
$('input[name=rowName_' + i + ']').rules("add", {
        isAlphabet: true
});
```

## ポイント④ : 値チェックをするaddMethodの中身には、HTMLの各要素を入れても大丈夫

値チェックの処理自体は、動的な制約指定の前に追加しても大丈夫なようです。
ここではjqueryのvalue selectorを使って、色々柔軟に制約を使うことも出来ると思います。
出来れば、色々な列で使えるように一般化できるとなおよし。

```js
$.validator.addMethod('isAlphabet',
    function(value, element, param) {
        /* ポイント④ : 
         addMethodの中身には、HTMLの各要素を入れても大丈夫
        */

        condition = value.match(/^[\w\s]+$/g);
        if (condition) {
            $('input[name=' + element.name + ']').removeClass('bg-warning');
            return true;
        } else {
            $('input[name=' + element.name + ']').addClass('bg-warning');
            return false;
        }
    },
    jQuery.validator.format('このフィールドはアルファベット、数字、アンダーバー(_)、空白のみ指定できます。'));
```

## ポイント⑤ : validateは空の状態でも実行しないと、値チェックは行われない。

抜け落ちやすいので注意しましょう。
スクリプト中で、**validateを最低１回でも実行しないと**、validationは行われません。
空で良いです。

```js
$('form').validate();
```

## ソースコード

```html:HTML部分
<!--  ポイント① : data-menuindexとしメニューのクリックした行を確保できるようにする -->
<div id="contextmenu" class="text-left" style="display: none;" data-menuindex="0">
    <table class="table table-hover table-info table-sm">
        <tr onClick="pressMenu('remove')">
            <td>削除</td>
        </tr>
    </table>
</div>

<form action="~/AnyData/Index" method="post">
    <div class="text-left">
        <table class="table table-primary" id="root_table">
            <thead>
            <tr>
                <th>ID</th>
                <th>名前</th>
                <th>式</th>
            </tr>
            </thead>
        </table>
    </div>
    <div class="text-left">
        <input type="submit" value="送信" class="btn btn-primary"/>
    </div>
</form>
```

```js:JavaScript(Menu部分)
function pressMenu(menu_item) {
    rowindex = $('#contextmenu').data['rowindex'];

    if (menu_item === "remove") {
        // ここでは行を単にHTMLから消しているだけだが、
        // APIなどの通信なしでサーバサイドとの連携するときは、
        // ajaxでDELETEリクエストを送る
        $('#root_table').DataTable().row(
           $('label[name=id_' + rowindex + ']').parents("tr")).remove().draw();
    }
}

// メニュー以外押したらメニューを解除
$(document).on('click', function(e) {
   $('#contextmenu').css('display', 'none');
});
```


```js:JavaScript(DataTables部分)

$(document).ready(function () {
    datatable = $('#root_table').DataTable({
        ajax: {
            url: "/api/AnyData",
            dataSrc: ""
        },
        columns: [
            {
                data: "id",
                render: function(data, type, instance, meta) {
                    /* ポイント② : 行の識別子としてmeta.rowを使用する */
                    return '<label name="id_' + meta.row + '" 
                             class="js-text-input" data-row="' + meta.row + '">' + data + '</label>';
                }
            },
            {
                data: "name",
                render: function(data, type, instance, meta) {
                    return '<input type="text" name="rowName_' + meta.row + '" 
                             class="js-text-input" value="' + data + '"/>';
                }
            },
            {
                data: "expression",
                render: function(data, type, instance, meta) {
                    return '<input type="text" name="expression_' + meta.row + '" 
                             class="js-text-input" value="' + data + '"/>';
                }
            }
        ],
        ordering: false,      // ソートも禁止する
        searching: false,     // 検索バーを消す
        info: false,          // 情報表示も消す
        paging: false,        // テーブル画面のPreviousやNextは消す
        initComplete: function(settings, ajax_json) {
            // もしDataTablesの描画が終わったら、
            // テキストボックスの色を変えたり
            // validationルールを加えたりする.
            for (i = 0; i < ajax_json.length; i++) {
                /* ポイント③ : 
                   validatorをinitCompleteの中に入れて、 
                   動的にvalidation制約を追加する
                */
                $('input[name=rowName_' + i + ']').rules("add", {
                        isAlphabet: true
                });

                $('input[name=expression_' + i + ']').rules("add", {
                        isValidExpression: true
                });

                // メニュー右クリック
                $('label[name=id_' + i + ']')[0].oncontextmenu = function(e) {
                    $('#contextmenu').css({ 'position': 'absolute', 'left': e.pageX, 'top': e.pageY,
                                            'display': 'block' });
                    // contextmenuの行の添え字を登録するのを忘れずに
                    $('#contextmenu').data['rowindex'] = this.dataset['row'];

                    // return falseとすることで、
                    // ブラウザの右クリックを排除できる
                    return false;
                };
            }
        }
    });

    // DataTables Validationの定義
    // 名前判定
    $.validator.addMethod('isAlphabet',
        function(value, element, param) {
            /* ポイント④ : 
             addMethodの中身には、HTMLの各要素を入れても大丈夫
            */

            condition = value.match(/^[\w\s]+$/g);
            if (condition) {
                $('input[name=' + element.name + ']').removeClass('bg-warning');
                return true;
            } else {
                $('input[name=' + element.name + ']').addClass('bg-warning');
                return false;
            }
        },
        jQuery.validator.format('このフィールドはアルファベット、数字、アンダーバー(_)、空白のみ指定できます。'));

    // 式判定
    $.validator.addMethod('isValidExpression',
        function (value, element, param) {
            condition = value.match(/^[\s|\$\d+|\d|\=\+\-\*\/]+$/g);
            if (condition) {
                $('input[name=' + element.name + ']').removeClass('bg-warning');
                return true;
            } else {
                $('input[name=' + element.name + ']').addClass('bg-warning');
                return false;
            }
        },
        jQuery.validator.format('正しい式ではありません'));

    /* ポイント⑤ : 
     validateは空の状態でも実行しないと、値チェックは行われない。
    */
    // 一度空のvalidateを入れないと
    // フォーム全体ではvalidationしてくれない
    $('form').validate();
});

```

# 感想

jqueryもdatatablesも大変だが、扱えたら素晴らしいUIが作れることがよくわかる。

私はアプリケーション系の開発経験がWeb系の開発経験よりも多いのだが、
こうしたDataTablesを使った機能拡張は結構アプリケーションだとやりにくいことが多い。
上手いことWebの強みを生かすときに、あると便利だなぁと思いました。