##SlickGridを使ってみます。
###1.なぜ、SlickGridなのか
・WEBアプリ
・インラインのCRUDができる(編集行は2000行程度可能であること。)
・インラインはフィルタができること
という要件を満たす必要があり、軽量なgirdライブラリである「SlickGrid」を利用することにしました。
###2.ダウンロード
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セルを作る
カスタムエディタ。selectBoxを使うためのソースは参考サイトのまんまです。
(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).作成したソース
かなり長いソースになってしまいましたが、サンプルのまんまで、それほど難解ではありません。
<!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)));
});
});
50000行の編集画面ですが、軽快にうごきます。
あとは、データをajaxで受信し、選択した行をサーバに送信して、サーバサイドで更新処理を実装してしまえば、サーバ上のある程度大量のデータ数をもつテーブルのインライン編集画面が実現できそうです。