Excel風一覧から一括の登録、更新、削除(更新、削除編)

投稿者: | 2019年12月16日

WEBでExcel風の一覧を再現するHandsontableを使って、一括更新、登録、削除機能を実装しました。今回は「一括更新、削除編」です。

続いて更新、削除です。まず更新ですが、セルを編集した場合にステータスを更新に変更します。先の記事でも書きましたが、何もしていないデータにはサーバ側でも何もしないようにという意図です。ただし、諦めている点もあります。編集して「更新」ステータスにした後、もともと画面に表示していた値に再度戻したとしても「更新」状態のままです。

更新時の実装

初期状態

2行目を編集して「値を変更した」場合は下記のようになります。この状態で反映を押下すると、サーバ側では2行目だけ処理して、1、3行目には何もしません。

画面側の実装

hot.addHook('afterChange', concreateAfterChange(hot, "status", ["id","status","select", "_status"]));

ライブラリ側の実装

考慮すべき点は、サーバ側のことを考えると当然ですが、「追加」はUpdateにしません。また、サーバ側で読み取る前提ではない項目は判定から除外することです。例えば、ステータスそのものや、削除選択用のチェックボックスの値は判定から除外しています。なお「(changes, source) 」は言うまでもなくHandsontableのafterChange時に実行されるファンクションのインターフェイスがこれであるから、というお約束事です。

function concreateAfterChange(hot, statusCol, ignoreCols = undefined) {

    var result = function(changes, source) {
        if (source === 'loadData') {
            return;
        }
        for (var i = 0; i < changes.length; i++) {
            var change = changes[i];

            // 新規行はUPDにしない.
            if (hot.getDataAtRowProp(change[0], statusCol) == INS) {
                continue;
            }
            // ステータスや選択は対象外。汎用化して項目をリストで受取
            if (!(ignoreCols === undefined) && ignoreCols.indexOf(change[1]) >= 0) {
                continue;
            }
            // 変更前と変更後が同じは対象外
            if (change[2] === change[3]) {
                continue;
            }

            // 編集に"UPD"を付ける
            hot.setDataAtRowProp(change[0], statusCol, UPD);
        }
    }
    return result;
}

削除時の実装

削除するときは、DBから読み込んだ行については「削除」の印をつけてサーバ側に送信します。そうでないと、クライアント側からデータでの洗い替えになってしまいます。検索条件で一部を抽出して更新するときには、このような洗い替え方式は破綻するので、削除フラグを立てて送信する方式になるよう考えました。

初期状態

削除ボタン押下後

ここでもう一つ考慮したことがあります。行追加した行の削除と、DBから読み取った行の削除では都合が違うという点です。後者は先に説明したように削除できるようステータスを更新して送信します。前者は、サーバ側に送ってもごみになるだけなので物理的に削除します。

新規行を削除した場合

画面側の実装

今回は、削除できる条件として「工数確定回数」が0の時だけというものを設定しています。そのような削除を許可する仕組みもある程度簡単につけられるように考えてみました。(=trueを返すと削除できる)

$('#removeBtn').removeRowHot(hot, "status", function(hot, i){
    var r = hot.getDataAtCell(i, "results")
    return hot.getDataAtCell(i, "select") && (r == 0 || r == null);
});

続いてライブラリ側です

const DEL = "Del";
(function($) {
    'user strict';
:
    var delFunc = function(hot, i, statusCol) {
        var stat = hot.getDataAtRowProp(i, statusCol);
        if (stat == INS) {
            hot.alter('remove_row', i);
            return;
        }
        if (stat == DEL) {
            var bstatus = hot.getDataAtRowProp(i, "_" + statusCol);
            if (bstatus == null) { bstatus = ""; }
            hot.setDataAtRowProp(i, statusCol, bstatus);
            hot.setDataAtRowProp(i, "_" + statusCol, "");
            return;
        }
        hot.setDataAtRowProp(i, "_" + statusCol, hot.getDataAtRowProp(i, statusCol));
        hot.setDataAtRowProp(i, statusCol, DEL);
    }
:(中略)
    $.fn.removeRowHot = function(hot, statusCol, removableFunc = undefined, afterFunc = undefined) {
        this.bind('click', function(e) {
            e.preventDefault();
            e.stopPropagation()

            for (var i = hot.countRows() - 1; i >= 0; i--) {

                if (removableFunc === undefined) {
                    delFunc(hot, i, statusCol);
                    continue;
                }
                if (removableFunc(hot, i)) {
                    delFunc(hot, i, statusCol);
                    continue;
                }
            }

            if (afterFunc === undefined) {} else {
                afterFunc();
            }
        });
    }

注意点としては、物理削除することがあるので、行は後ろから削除しています。よくある「定石」ですね。ついでなので、「if (stat == DEL) 」この辺りから「削除の取り消し」を実装しています。削除している行に再度削除命令を出すと削除の印が取り消されます。

削除前

削除後

削除行を再度削除して取り消し

以上で、一括の登録、更新、削除をするための仕組みづくりができました。これから先は、クライアント側で編集したデータをサーバに送信する仕組みを書いていきます。Ruby on Railsが前提なので、link_toのヘルパーも出てきます。

myなんちゃってライブラリのお約束

上記のようなステータス制御が入る関係上、下記のような定義がhandsontableに必須です。これらを書くため、列幅で「見えないように」しています。気にする場合には、classnameも設定できるので、display:noneなクラスを当ててしまってもいいかもしれません。

var hot = new Handsontable(grid, {
    data: data,
    :(中略)
    columns: [
		:(中略)
        { data: '_status'  , type: 'text'   ,
            readOnly: true },
        { data: 'status'   , type: 'text'   ,
            readOnly: true },
        { data: "select"   , type: 'checkbox' },
        (以下略)