SlickGridを使ってみる

##SlickGridを使ってみます。

###1.なぜ、SlickGridなのか

・WEBアプリ
・インラインのCRUDができる（編集行は2000行程度可能であること。）
・インラインはフィルタができること
という要件を満たす必要があり、軽量なgirdライブラリである「SlickGrid」を利用することにしました。

###2.ダウンロード
 [github.com/6pac/6pac.github.io](https://github.com/6pac/6pac.github.io)からダウンロードします。

 ※　https://github.com/mleibman/SlickGrid
こちらは古いバージョンのようです。こちらは使いません。

###3.実装
ダウンロードしたファイルを解凍すると、「examples」というフォルダの中に使用例がたくさんあります。

####(1)ベースとなるhtml
「example4-model.html」が今回の要件に一番近いので、それを編集して作成しました。

####(2). selectBoxで編集可能にする。
インラインで編集する場合に、SelectBoxで編集するエディタが必要です。
SelectをEditorにする方法は、以下に投稿がありましたので、そちらを参考にしてください。
[【SlickGrid】Value,Label付きのselectセルを作る](https://qiita.com/Popphron/items/339710a77e1df84131bb)

カスタムエディタ。selectBoxを使うためのソースは参考サイトのまんまです。

```slick.editors.select.js
(function ($) {
    $.extend(true, window, {
        "Extends": {
            "Editors": {
                "Select": SelectEditor
            },
            "Formatters": {
                "Select": SelectFormatter
            }
        }
    });

    function SelectEditor(args) {
        var $select;
        var defaultValue;
        var scope = this;

        this.init = function () {
            var option_str = "";
            for( key in args.column.options ){
                option_str += "<OPTION value='" + key +"'>" + args.column.options[key] + "</OPTION>";
            }
            $select = $("<SELECT tabIndex='0' class='editor-select'>"+ option_str +"</SELECT>");
            $select.appendTo(args.container);
            $select.focus();
        };

        this.destroy = function () {
            $select.remove();
        };

        this.focus = function () {
            $select.focus();
        };

        this.loadValue = function (item) {
            defaultValue = item[args.column.field];
            $select.val(defaultValue);
        };

        this.serializeValue = function () {
            return $select.val();
        };

        this.applyValue = function (item, state) {
            item[args.column.field] = state;
        };

        this.isValueChanged = function () {
            return ($select.val() != defaultValue);
        };

        this.validate = function () {
            return {
                valid: true,
                msg: null
            };
        };

        this.init();
    }

    function SelectFormatter(row, cell, value, columnDef, dataContext) {
        //valueがnullのときにundefinedになるので
        if (columnDef.options[value] == undefined){
          return "";
        }
        return columnDef.options[value];
    }

})(jQuery);
```

####(3). 選択した行を記憶する
「example-checkbox-row-select.html」を参考にして、チェックボックスを実装します。

####(4).作成したソース

かなり長いソースになってしまいましたが、サンプルのまんまで、それほど難解ではありません。

```slick.html
<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <link rel="shortcut icon" type="image/ico" href="favicon.ico" />
  <title>SlickGrid example 4: Model</title>
  <link rel="stylesheet" href="../slick.grid.css" type="text/css"/>
  <link rel="stylesheet" href="../controls/slick.pager.css" type="text/css"/>
  <link rel="stylesheet" href="../../../lib/jquery-ui-1.12.1.custom/jquery-ui.min.css" type="text/css"/>
  <link rel="stylesheet" href="examples.css" type="text/css"/>
  <link rel="stylesheet" href="../controls/slick.columnpicker.css" type="text/css"/>
  <style>
    .cell-title {
      font-weight: bold;
    }

    .cell-effort-driven {
      text-align: center;
    }

    .cell-selection {
      border-right-color: silver;
      border-right-style: solid;
      background: #f5f5f5;
      color: gray;
      text-align: right;
      font-size: 10px;
    }

    .slick-row.selected .cell-selection {
      background-color: transparent; /* show default selected row background */
    }
    
    .editor-select{
      width:100%;
    }
  </style>
</head>
<body>

<div style="position:relative">
  <div style="width:760px;">
    <label>絞り込み:</label>
    <input type=text id="txtSearch" style="width:100px;">
    <button id="btnSelectRows">Select first 10 rows</button>
    <div id="myGrid" style="width:100%;height:500px;"></div>
    <div id="pager" style="width:100%;height:20px;"></div>
  </div>
  <br/>
  <button type="button" id="show-selected">選択した行をログに出力する</button>
</div>

<script src="../lib/firebugx.js"></script>
<script src="../lib/jquery-3.3.1.min.js"></script>
<script src="../lib/jquery-ui-1.12.1.min.js"></script>
<script src="../lib/jquery.event.drag-2.3.0.js"></script>
<script src="../slick.core.js"></script>
<script src="../plugins/slick.checkboxselectcolumn.js"></script>
<script src="../slick.formatters.js"></script>
<script src="../slick.editors.js"></script>
<!-- カスタムエディタ。selectBoxを使うためのソース -->
<script src="../slick.editors.select.js"></script>
<script src="../plugins/slick.rowselectionmodel.js"></script>
<script src="../slick.grid.js"></script>
<script src="../slick.dataview.js"></script>
<script src="../controls/slick.pager.js"></script>
<script src="../controls/slick.columnpicker.js"></script>

<script>
function isIEPreVer9() { var v = navigator.appVersion.match(/MSIE ([\d.]+)/i); return (v ? v[1] < 9 : false); }

<!-- SelectBoxのデータ。grid上のselectボックスのプルダウンに表示されます -->
var my_options = {
  "1": "AAA",
  "2": "BBB",
  "3": "CCC"
};

var dataView;
var grid;
var data = [];
var checkboxSelector1;
var columns = [];
<!-- (6)CheckBoxの全選択ボタンの表示設定 -->
var isSelectAllCheckbox1Hidden = false;

var columns = [];

<!-- (7)CheckBox用の列オブジェクト -->
checkboxSelector1 = new Slick.CheckboxSelectColumn({
  cssClass: "slick-cell-checkboxsel"
});
columns.push(checkboxSelector1.getColumnDefinition());
<!-- チェックボックス以外は、Exsampleのままです。 -->
columns.push({id: "title", name: "Title", field: "title", width: 120, cssClass: "cell-title", editor: Slick.Editors.Text, validator: requiredFieldValidator, sortable: true});
columns.push({id: "desc", name: "Description", field: "description", width: 100, editor: Slick.Editors.LongText, sortable: true});
columns.push({id: "duration", name: "Duration", field: "duration", editor: Slick.Editors.Text, sortable: true});
columns.push({id: "%", name: "% Complete", field: "percentComplete", width: 80, resizable: false, formatter: Slick.Formatters.PercentCompleteBar, editor: Slick.Editors.PercentComplete, sortable: true});
columns.push({id: "start", name: "Start", field: "start", minWidth: 60, editor: Slick.Editors.Date, sortable: true});
columns.push({id: "finish", name: "Finish", field: "finish", minWidth: 60, editor: Slick.Editors.Date, sortable: true});
columns.push({id: "effort-driven", name: "Effort Driven", width: 80, minWidth: 20, maxWidth: 80, cssClass: "cell-effort-driven", field: "effortDriven", formatter: Slick.Formatters.Checkmark, editor: Slick.Editors.Checkbox, sortable: true});

<!-- 最終列にselectBoxの編集列を追加する -->
columns.push({id: "select",
  name: "Select",
  field: "select",
  formatter: Extends.Formatters.Select,
  editor: Extends.Editors.Select,
  options: my_options
});

var options = {
  columnPicker: {
    columnTitle: "Columns",
    hideForceFitButton: false,
    hideSyncResizeButton: false, 
    forceFitTitle: "Force fit columns",
    syncResizeTitle: "Synchronous resize",
  },
  editable: true,
  enableAddRow: true,
  enableCellNavigation: true,
  asyncEditorLoading: true,
  forceFitColumns: false,
  topPanelHeight: 25
};

var sortcol = "title";
var sortdir = 1;
var percentCompleteThreshold = 0;
var searchString = "";

function requiredFieldValidator(value) {
  if (value == null || value == undefined || !value.length) {
    return {valid: false, msg: "This is a required field"};
  }
  else {
    return {valid: true, msg: null};
  }
}

function myFilter(item, args) {
  if (item["percentComplete"] < args.percentCompleteThreshold) {
    return false;
  }

  if (args.searchString != "" && item["title"].indexOf(args.searchString) == -1) {
    return false;
  }

  return true;
}

function percentCompleteSort(a, b) {
  return a["percentComplete"] - b["percentComplete"];
}

function comparer(a, b) {
  var x = a[sortcol], y = b[sortcol];
  return (x == y ? 0 : (x > y ? 1 : -1));
}

function toggleFilterRow() {
  grid.setTopPanelVisibility(!grid.getOptions().showTopPanel);
}

$(".grid-header .ui-icon")
        .addClass("ui-state-default ui-corner-all")
        .mouseover(function (e) {
          $(e.target).addClass("ui-state-hover")
        })
        .mouseout(function (e) {
          $(e.target).removeClass("ui-state-hover")
        });

$(function () {

  // prepare the data
  for (var i = 0; i < 50000; i++) {
    data.push( {"id":"id_" + i, "num": i, "title": "Task " + i, "duration": "5 days", "percentComplete": Math.round(Math.random() * 100), "start": "01/01/2009", "finish": "01/05/2009", "effortDriven": (i % 5 == 0)});
  }

  dataView = new Slick.Data.DataView({ inlineFilters: true });
  grid = new Slick.Grid("#myGrid", dataView, columns, options);
  grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: false}));
  //チェックボックスをregist
  grid.registerPlugin(checkboxSelector1);

  var pager = new Slick.Controls.Pager(dataView, grid, $("#pager"));
  var columnpicker = new Slick.Controls.ColumnPicker(columns, grid, options);

  // move the filter panel defined in a hidden div into grid top panel
  $("#inlineFilterPanel")
      .appendTo(grid.getTopPanel())
      .show();

  grid.onCellChange.subscribe(function (e, args) {
    dataView.updateItem(args.item.id, args.item);
  });

  grid.onAddNewRow.subscribe(function (e, args) {
    var item = {"num": data.length, "id": "new_" + (Math.round(Math.random() * 10000)), "title": "New task", "duration": "1 day", "percentComplete": 0, "start": "01/01/2009", "finish": "01/01/2009", "effortDriven": false};
    $.extend(item, args.item);
    dataView.addItem(item);
  });

  grid.onKeyDown.subscribe(function (e) {
    // select all rows on ctrl-a
    if (e.which != 65 || !e.ctrlKey) {
      return false;
    }

    var rows = [];
    for (var i = 0; i < dataView.getLength(); i++) {
      rows.push(i);
    }

    grid.setSelectedRows(rows);
    e.preventDefault();
  });

  grid.onSort.subscribe(function (e, args) {
    sortdir = args.sortAsc ? 1 : -1;
    sortcol = args.sortCol.field;

    if (isIEPreVer9()) {
      // using temporary Object.prototype.toString override
      // more limited and does lexicographic sort only by default, but can be much faster

      var percentCompleteValueFn = function () {
        var val = this["percentComplete"];
        if (val < 10) {
          return "00" + val;
        } else if (val < 100) {
          return "0" + val;
        } else {
          return val;
        }
      };

      // use numeric sort of % and lexicographic for everything else
      dataView.fastSort((sortcol == "percentComplete") ? percentCompleteValueFn : sortcol, args.sortAsc);
    } else {
      // using native sort with comparer
      // preferred method but can be very slow in IE with huge datasets
      dataView.sort(comparer, args.sortAsc);
    }
  });

  // wire up model events to drive the grid
  // !! both dataView.onRowCountChanged and dataView.onRowsChanged MUST be wired to correctly update the grid
  // see Issue#91
  dataView.onRowCountChanged.subscribe(function (e, args) {
    grid.updateRowCount();
    grid.render();
  });

  dataView.onRowsChanged.subscribe(function (e, args) {
    grid.invalidateRows(args.rows);
    grid.render();
  });

  dataView.onPagingInfoChanged.subscribe(function (e, pagingInfo) {
    grid.updatePagingStatusFromView( pagingInfo );
  });

  // wire up the search textbox to apply the filter to the model
  $("#txtSearch").keyup(function (e) {
    Slick.GlobalEditorLock.cancelCurrentEdit();

    // clear on Esc
    if (e.which == 27) {
      this.value = "";
    }

    searchString = this.value;
    updateFilter();
  });

  function updateFilter() {
    dataView.setFilterArgs({
      percentCompleteThreshold: percentCompleteThreshold,
      searchString: searchString
    });
    dataView.refresh();
  }
  $("#btnSelectRows").click(function () {
    if (!Slick.GlobalEditorLock.commitCurrentEdit()) {
      return;
    }

    var rows = [];
    for (var i = 0; i < 10 && i < dataView.getLength(); i++) {
      rows.push(i);
    }

    grid.setSelectedRows(rows);
  });

  $("#show-selected").click(function () {
    var selectedrows = grid.getSelectedRows();

selectedrows.forEach(function( value ) { 
      alert(JSON.stringify(dataView.getItem(value)));
});
  });

  // initialize the model after all the events have been hooked up
  dataView.beginUpdate();
  dataView.setItems(data);
  dataView.setFilterArgs({
    percentCompleteThreshold: percentCompleteThreshold,
    searchString: searchString
  });
  dataView.setFilter(myFilter);
  dataView.endUpdate();

  // if you don't want the items that are not visible (due to being filtered out
  // or being on a different page) to stay selected, pass 'false' to the second arg
  dataView.syncGridSelection(grid, true);

  $("#gridContainer").resizable();
})
</script>
</body>
</html>
```

選択した行のオブジェクトは、以下のような感じで取得できます。

```
  $("#show-selected").click(function () {
    var selectedrows = grid.getSelectedRows();

selectedrows.forEach(function( value ) { 
      alert(JSON.stringify(dataView.getItem(value)));
});
  });
```


###3.動作確認
![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/79e28e2f-6e37-332b-54ad-d64539133684.png)

50000行の編集画面ですが、軽快にうごきます。

あとは、データをajaxで受信し、選択した行をサーバに送信して、サーバサイドで更新処理を実装してしまえば、サーバ上のある程度大量のデータ数をもつテーブルのインライン編集画面が実現できそうです。