4. テーブル¶
この章では、テーブルのライブラリであるSlickGrid、tablesorterを用いた実装例を説明する。
それぞれのライブラリが実現できる機能は、次の表の通りである。
| 機能 | SlikGrid | tablesorter | 
|---|---|---|
| 行追加・削除・編集 | ✔ | ✔ | 
| 行の並び替え | ✔ | ✗ | 
| ヘッダを固定したデータ行のスクロール | ✔ | ✔ | 
| ページネーション | ✔ | ✔ | 
| ソート | ✔ | ✔ | 
| カラムの並び替え | ✔ | ✗ | 
| コピーアンドペースト編集 | ✔ | ✗ | 
| テーブルのスクロールによる非同期データ取得 | ✔ | ✗ | 
どちらのライブラリを使用するかは、上に示した機能や、特徴 (SlickGrid概要およびtablesorter概要) を参考にして判断すること。
また、SlickGridとBootstrapを同時に使用する場合、付録の注意点を考慮すること。
Note
SlickGridはテーブルのセル数に応じてDOMが変化する。また、スクロールを行い表示内容が変化するタイミングでDOMの書き換えが発生する。 テーブルのセル数が多い場合、スクロール操作の度に多数のDOMを書き換える必要があり、大きなオーバーヘッドが発生することがあるため、十分に検証を行った上で採用すること。
4.1. ライブラリの基本的な使用方法¶
ここでは、テーブルを扱うライブラリであるSlickGridとtablesorterの基本的な使用方法を説明する。
| 利用ライブラリ | サンプル | 参考ページ | 
|---|---|---|
| SlickGrid | SlickGrid基本構成サンプル | SlickGrid Wiki | 
| tablesorter | tablesorter基本構成サンプル | jQuery plugin: Tablesorter 2.0 | 
4.1.1. SlickGrid基本構成サンプル¶
HTMLでは、SlickGrid用のスタイルシート、依存ライブラリ、SlickGridモジュール、さらにSlickGridでテーブルを生成するために実装したJavaScriptファイル(js/default.js)を読み込む。
<!DOCTYPE html>
<!-- Copyright(c) 2018 NTT Corporation. -->
<html>
  <head>
    <meta charset="utf-8">
    <title>SlickGrid基本構成サンプル</title>
    <!-- SlickGrid用のスタイルシートの読み込み -->
    <link rel="stylesheet" href="../lib/vendor/slickgrid/2.3.19/slick.grid.css">
    <link rel="stylesheet" href="css/main.css">
  </head>
  <body>
    <h1>SlickGrid基本構成サンプル</h1>
    <!-- SlickGridテーブルの表示領域となる要素 -->
    <div id="myGrid" class="grid" style="grid"></div>
    <!-- 依存ライブラリの読み込み -->
    <script src="../lib/vendor/jquery/3.3.1/jquery.min.js"></script>
    <script src="../lib/vendor/jquery.event.drag-drop/2.3.0/jquery.event.drag.js"></script>
    <script src="../lib/vendor/jquery-ui/1.12.1/jquery-ui.min.js"></script>
    <!-- SlickGridモジュールの読み込み -->
    <script src="../lib/vendor/slickgrid/2.3.19/slick.core.js"></script>
    <script src="../lib/vendor/slickgrid/2.3.19/slick.grid.js"></script>
    <!-- 実装したJavaScriptファイルの読み込み -->
    <script src="js/default.js"></script>
  </body>
</html>
JavaScript(js/default.js)では、Slick.Gridコンストラクタを実行することで、指定した領域にテーブルを生成する。
Slick.Gridコンストラクタのシグネチャは次のとおり。
- 
Slick.Grid(container, data, columns[, options]) Arguments: - container (String) – テーブルの表示領域となる要素のセレクタ
 - data (Array|DataView|Object) – テーブルで扱うデータ配列。データの表示条件をより詳細に制御する場合は、配列の代わりに、
DataViewオブジェクトや、getLengthとgetItem関数を持つオブジェクトを指定することもできる。 - columns (Array) – カラム定義。各カラムごとに表示名やデータ項目などのプロパティをもつオブジェクトを配列で複数指定する。
 - options (Object) – 動作オプション
 
/*
 *
 * Copyright(c) 2018 NTT Corporation.
 */
// default.js
'use strict';
(function () {
  // (1) SlickGridのカラム定義
  //     6つのカラムのそれぞれにつき ID・表示名・マッピングするデータ を定義している。
  var columns = [
    {id: 'title', name: 'Title', field: 'title'},
    {id: 'duration', name: 'Duration', field: 'duration'},
    {id: '%', name: '% Complete', field: 'percentComplete'},
    {id: 'start', name: 'Start', field: 'start'},
    {id: 'finish', name: 'Finish', field: 'finish'},
    {id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven'}
  ];
  // (2) SlickGridの動作オプション
  var options = {
    // 何も指定しない。
  };
  // (3) 1000件のサンプルデータ作成
  var data = [];
  for (var i = 0; i < 1000; i++) {
    // データのプロパティ名を columns で定義した field 値と一致させることで、
    // 表示データがマッピングされる。
    data[i] = {
      title: 'タスク ' + i,
      duration: '5 days',
      percentComplete: Math.round(Math.random() * 100),
      start: '01/01/2009',
      finish: '01/05/2009',
      effortDriven: (i % 5 === 0) ? 'true' : 'false'
    };
  }
  // 画面初期化処理
  $(function () {
    // (4) SlickGridテーブルを作成
    new Slick.Grid('#myGrid', data, columns, options);
  });
}());
カラム定義は、ソースコードリスト上の(1)で行っている。この例では6列のカラムを定義し、それぞれIDやヘッダの表示テキスト(name)、表示データ名(field)を指定している。
オプションの設定は(2)で行う。この例は基本構成なのでオプションは指定しないが、以降のサンプルではここに設定を追加していく。
データの作成は(3)で行っている。この例では1000件のデータを作成している。各データのtitleなどのデータは、カラム定義のfieldプロパティと一致するカラムに表示される。意図した通りにデータが表示されない場合は、カラム定義とデータのプロパティ名が一致していない可能性があるため、見直すこと。
最後に(4)で、これらの設定値やデータを用いて Slick.Grid コンストラクタを実行しテーブルを作成している。
Note
SlickGridには様々な依存ライブラリやスタイルシート、機能拡張用モジュールがあり、使用する機能に応じてこれらを読み込む必要がある。以下に、それぞれのファイルがどのような際に必要とされるかをまとめている(用途の少ないものは省略している)。
依存ライブラリ¶ 依存ライブラリ名 必要なケース jQuery 常に必要 jQuery UI Sortable 常に必要[1] jquery.event.drag 常に必要 jquery.event.drop 常に必要[2] 
SlickGrid用スタイルシート¶ ファイル名 必要なケース slick.grid.css 常に必要 css/smoothness/jquery-ui-1.8.16.custom.css jQuery UIのスタイルを適用する場合、またはページネーション機能を要する場合 controls/slick.pager.css ページネーション機能を要する場合 
SlickGridモジュール¶ ファイル名 必要なケース slick.core.js 常に必要 slick.grid.js 常に必要 slick.editors.js データの編集機能を要する場合 slick.dataview.js データの表示条件を詳細に制御する必要のある場合[3] plugins/slick.cellrangeselector.js セル選択機能を要する場合(ドラッグアンドドロップでの選択操作を可能にする) plugins/slick.cellrangedecorator.js セル選択機能を要する場合(選択範囲を可視化する) plugins/slick.cellselectionmodel.js セル選択機能を要する場合(データ選択範囲をプログラムから変更可能にする) plugins/slick.cellcopymanager.js コピーアンドペースト機能を要する場合 plugins/slick.rowselectionmodel.js 行選択機能を要する場合 plugins/slick.rowmovemanager.js 行の並び替え機能を要する場合 controls/slick.pager.js ページネーション機能を要する場合 
| [1] | 公式ドキュメントではソート機能を有効化した場合のみ必要と記載されているが、他にカーソル操作での選択セル移動機能や、セルのコピーアンドペースト機能などにも使用されている。そのため、基本的には常に読み込んでおくほうが無難ではあるが、ファイルサイズが比較的大きいため、極力読み込ませずに済ませたい場合には、動作確認を充分に行うこと。 | 
| [2] | 公式ドキュメントでは依存ライブラリとして記載されているが、実際のところは利用されていない。よって、テストで正常に動作することが確認できれば、読み込まなくてもよい。 | 
| [3] | データ表示条件を詳細に制御できる部品DataViewが利用可能になる。これによって、ページネーション、複数カラムでのソート、検索、グループ化などが可能になる。詳細は https://github.com/6pac/SlickGrid/wiki を参照すること。 | 
Note
ウインドウの幅に併せてテーブルの幅を変更したい場合は、SlickGridの提供機能ではないが、次のように記述して対応できる。
// (2) SlickGridの動作オプション var options = { // テーブルの幅をコンテナの幅に合わせるオプションを指定 forceFitColumns: true };// 画面初期化処理 $(function () { // (4) SlickGridテーブルを作成 // SlickGridのインスタンスをgrid変数に格納 var grid = new Slick.Grid('#myGrid', data, columns, options); });// ウィンドウサイズが変更された際に発生するイベントで、 // コンテナ幅にテーブルのサイズを合わせるメソッドを実行する処理を追加 $(window).resize(function () { grid.resizeCanvas(); });
ただし、ウインドウサイズを変更するたびに処理が発生するため、動作がもたつくことがある。 頻繁にウインドウサイズの変更を求められる画面では注意すること。
Note
jquery.event.drag, jquery.event.drop については、公式サイト で提供されている最終リリースバージョンは 2.2 であるが、SlickGrid(6pac) の依存ライブラリバージョンとして非公認であるバージョン 2.3 の利用を推奨していることから、本ガイドラインのサンプルプログラムにおいてもバージョン 2.3 を使用する。詳細は https://github.com/6pac/SlickGrid/wiki を参照すること。
4.1.2. tablesorter基本構成サンプル¶
HTMLでは、tablesorter用のスタイルシート、依存ライブラリ、tablesorterモジュール、さらにtablesorterを有効にするために実装したJavaScriptファイル(js/default.js)を読み込む。
<!DOCTYPE html>
<!-- Copyright(c) 2018 NTT Corporation. -->
<html>
  <head>
    <meta charset="utf-8">
    <title>tablesorter基本構成サンプル</title>
    <!-- tablesorter用のスタイルシートの読み込み -->
    <link rel="stylesheet" href="../lib/vendor/jquery.tablesorter/2.30.7/css/theme.default.css">
  </head>
  <body>
    <h1>tablesorter基本構成サンプル</h1>
    <table class="table">
      <thead>
        <tr>
          <th>姓</th><th>名</th><th>年齢</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>毛利</td><td>正雄</td><td>2</td>
        </tr>
        <tr>
          <td>安田</td><td>さとみ</td><td>28</td>
        </tr>
        <tr>
          <td>坂</td><td>文夫</td><td>45</td>
        </tr>
        <tr>
          <td>小本</td><td>里香</td><td>18</td>
        </tr>
        <tr>
          <td>中居</td><td>大輔</td><td>22</td>
        </tr>
      </tbody>
    </table>
    <!-- 依存ライブラリの読み込み -->
    <script src="../lib/vendor/jquery/3.3.1/jquery.min.js"></script>
    <!-- tablesorterモジュールの読み込み -->
    <script src="../lib/vendor/jquery.tablesorter/2.30.7/js/jquery.tablesorter.min.js"></script>
    <!-- 実装したJavaScriptファイルの読み込み -->
    <script src="js/default.js"></script>
  </body>
</html>
JavaScript(js/default.js)では、table要素に対し、tablesorterメソッドを実行することで、指定したテーブルにtablesorterの動作を適用する。また、tablesorterメソッドのプロパティとしてオプションを指定することで、様々な動作を実現することができる。
/*
 *
 * Copyright(c) 2018 NTT Corporation.
 */
// default.js
'use strict';
$(function () {
  $('.table').tablesorter({
    // tablesorterの動作オプション
  });
});
Note
tablesorterには様々な依存ライブラリやスタイルシート、機能拡張用モジュールがあり、使用する機能に応じてこれらを読み込む必要がある。以下に、それぞれのファイルがどのような際に必要とされるかをまとめている(用途の少ないものは省略している)。
依存ライブラリ¶ 依存ライブラリ名 必要なケース jQuery 常に必須 
tablesorter用スタイルシート¶ ファイル名 必要なケース css/theme.default.css 常に必要 
tablesorterモジュール¶ ファイル名 必要なケース js/jquery.tablesorter.js 常に必要 js/widgets/widget-scroller.js ヘッダの固定を実現する場合 js/widgets/widget-editable.js データの編集機能を要する場合 addons/pager/jquery.tablesorter.pager.js ページネーション機能を要する場合 
Note
基本構成サンプルではdefaut.jsを読み込んでいるが、以降の節ではそれぞれ実装したJavaScriptファイルを読み込む。
Note
SlickGridではJavaScriptでセルの値を設定するのに対し、tablesorterではHTMLに直接マークアップするのが基本的な使い方である。
この仕組みのため、tablesorterではページのロード時に、tablesorterの処理が適用前のテーブルが一時的に表示されることがある。これを防ぎたい場合は、table要素のスタイルにdisplay: noneを指定した上で、JavaScriptで $('#tablesorter').tablesorter().show() のように記述することで、tablesorterの処理が完了後に表示されるようにすればよい。
4.2. 行追加・削除・編集¶
4.2.1. 概要¶
ここでは、SlickGridおよびtablesorterを用いて、テーブルのデータ行の追加・削除を行う方法を説明する。
| 利用ライブラリ | サンプル | 参考ページ | 
|---|---|---|
| SlickGrid | SlickGridによる行追加・削除・編集サンプル | SlickGrid Wiki | 
| tablesorter | tablesorterによる行追加・削除・編集サンプル | 
4.2.2. 利用方法¶
4.2.2.1. SlickGridによる行追加・削除・編集サンプル¶
行追加・削除・編集のためのユーザインタフェースにはいくつかのパターンが考えられるが、 この例では、次のユーザインタフェースでの実現方法を説明する。
| 行追加 | テーブルの最下行にデータ追加用の空白行を設ける。 | 
|---|---|
| 行削除 | 各行の最後のカラムにデータ削除ボタンを設ける。 | 
| データ編集 | 
  | 
図: 行追加・削除・編集ユーザインタフェース例
SlickGrid基本構成サンプルに示したHTMLに加え、次のモジュールを読み込む。それ以外は同様なので省略する。
| モジュール名 | 用途 | 
|---|---|
| slick.editors.js | データの編集機能を提供する。 | 
JavaScript(insert-and-delete-row.js)では、ユーザインタフェースの操作によって発生するイベントを監視し、行データの追加・削除を行う。
/*
 *
 * Copyright(c) 2018 NTT Corporation.
 */
// insert-and-delete-row.js
'use strict';
(function () {
  // SlickGridのカラム定義
  var columns = [
    // (1) データのテキスト編集を可能にするため `editor` プロパティに `Slick.Editors.Text` を指定する。
    {id: 'title', name: 'Title', field: 'title', editor: Slick.Editors.Text },
    {id: 'duration', name: 'Duration', field: 'duration', editor: Slick.Editors.Text },
    {id: '%', name: '% Complete', field: 'percentComplete', editor: Slick.Editors.Text },
    {id: 'start', name: 'Start', field: 'start', editor: Slick.Editors.Text },
    {id: 'finish', name: 'Finish', field: 'finish', editor: Slick.Editors.Text },
    {id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', editor: Slick.Editors.Text },
    // (2) 削除ボタンを表示するためのカラムを定義
    {id: 'delete', name: '削除', field: '', cssClass: 'delete', width: 10,
     formatter: function () { return '<button class="delete-button">x</button>'; } }
  ];
  // SlickGrid動作オプション
  var options = {
    // (3) 編集可能にする。
    editable: true,
    // (4) セル選択時に自動的に編集モードにしない。
    autoEdit: false,
    // (5) 行追加を可能にする。
    enableAddRow: true
  };
  // 5件のサンプルデータ作成
  var data = [];
  for (var i = 0; i < 5; i++) {
    data[i] = {
      title: 'タスク ' + i,
      duration: '5 days',
      percentComplete: Math.round(Math.random() * 100),
      start: '01/01/2009',
      finish: '01/05/2009',
      effortDriven: 1
    };
  }
  function formatDate(date) {
    return ('0' + (date.getMonth() + 1)).slice(-2) + '/' +
      ('0' + date.getDate()).slice(-2) + '/' +
      date.getFullYear();
  }
  // 新規追加されるアイテムのデフォルト値
  var defaultData = {
    title: '未設定',
    duration: '未設定',
    percentComplete: 0,
    start: formatDate(new Date()),
    finish: formatDate(new Date(+new Date() + 1000 * 60 * 60 * 24)),
    effortDriven: 'false'
  };
  // 画面初期化処理
  $(function () {
    // SlickGridテーブルを作成
    var grid = new Slick.Grid('#myGrid', data, columns, options);
    // (6) クリックイベントハンドラ
    grid.onClick.subscribe(function (e, args) {
      if ($(e.target).hasClass('delete-button')) {
        data.splice(args.row, 1);
        grid.invalidate();
      }
    });
    // (7) 行追加イベントハンドラ
    grid.onAddNewRow.subscribe(function (e, args) {
      // 追加されたアイテムにデフォルト値を設定してデータに追加し、更新する。
      var item = $.extend({}, defaultData, args.item);
      data.push(item);
      grid.updateRowCount();
      grid.invalidateRow(data.length - 1);
      grid.render();
    });
  });
}());
行追加機能は、ソースコードリスト上の(5)・(7)で実現している。
(5)でオプションenableAddRowをtrueにすることで、最下行にデータ追加用の空白行が表示されるようになり、
空白行に新たなデータが入力された際にonAddNewRowイベントが発生するようになる。このイベントを(7)で監視し、
空白行へ追加されたデータをdataに追加して、テーブルを再描画している。
行削除機能は、(2)・(6)で実現している。
(2)のカラム定義ではformatterプロパティにHTML文字列を返す関数を指定して、delete-buttonクラスを持つ
行削除用ボタンを作成している。このようにformatterプロパティに任意の文字列を返す関数を指定することで、
該当するカラムの各セル内にレンダリングされるHTMLを変更できる。
またテーブルがクリックされた際に発生するonClickイベントを(6)で監視し、
もしクリックされた要素が(2)で作成したdelete-buttonクラスを持つボタンであれば、クリックされた行のデータを
dataから削除して、テーブルを再描画している。
データ編集機能は、(1)・(3)・(4)で実現している。
(1)のカラム定義ではeditorプロパティにテキスト編集を可能にするSlick.Editors.Text[4]を指定し、
(3)でオプションeditableをtrueにすることで、セルのデータ編集が可能になる。
またカーソルキーで選択セルを移動できるようにするため、(4)でオプションautoEditをfalseにして
セル選択時に自動的に編集モードに入らないようにしている。
| [4] | テキスト編集をするSlick.Editors.Text以外にも、数値を編集するSlick.Editors.Integer、日付を編集するSlick.Editors.Dateなどの編集用オブジェクトが提供されている。ただしドキュメントが整備されていないため、何が利用できるかはslick.editors.jsのソースコードを参照する必要がある。 | 
Note
SlickGridでは、データの追加や削除などを画面に反映するためにinvalidateおよびrenderなどのメソッドを実行する必要がある。
その際に使用するメソッドを以下に示す。
SlickGridの主なメソッド¶ メソッド名 説明 grid.render() 再描画対象になっている行を再描画する。再描画対象を設定するには invalidateRowなどのメソッドを使用する。grid.invalidateRow(Number) 特定行を再描画対象にする。引数には行インデックス番号(0から開始)を示す数値を与える。 grid.invalidateRows(Array<Number>) 複数の特定行を再描画対象にする。引数には行インデックス番号(0から開始)を示す数値の配列を与える。 grid.invalidateAllRows() 全ての行を再描画対象にする。 grid.invalidate() すべての行を再描画する。 renderメソッドを実行しなくても再描画される。
4.2.2.2. tablesorterによる行追加・削除・編集サンプル¶
この例では、次のユーザインタフェースを設けることで実現する方法を説明する。
| 行追加 | ボタンクリックで、テーブルの最下行にデータを追加する。 | 
|---|---|
| 行削除 | 各行の最後のカラムにデータ削除ボタンを設ける。 | 
| データ編集 | セルをクリックすると編集モードに入る。 | 
図: 行追加・削除・編集ユーザインタフェース例
tablesorter基本構成サンプルで示したHTMLに加え、次のモジュールを読み込む。
| モジュール名 | 用途 | 
|---|---|
| widget-scroller.js | データの編集機能を提供する。 | 
また、各データ行に削除ボタンを用意する。以下に該当箇所を抜粋する。
    <!-- tablesorterを使用するテーブル -->
    <table id="tablesorter">
      <thead>
        <tr><th>姓</th><th>名</th><th>年齢</th><th>削除</th></tr>
      </thead>
      <tbody>
        <tr><td><div>毛利</div></td><td><div>正雄</div></td><td><div>28</div></td><td><button class="remove-btn">x</button></td></tr>
        <tr><td><div>安田</div></td><td><div>さとみ</div></td><td><div>28</div></td><td><button class="remove-btn">x</button></td></tr>
        <tr><td><div>坂</div></td><td><div>文夫</div></td><td><div>45</div></td><td><button class="remove-btn">x</button></td></tr>
        <tr><td><div>小本</div></td><td><div>里香</div></td><td><div>18</div></td><td><button class="remove-btn">x</button></td></tr>
        <tr><td><div>中居</div></td><td><div>大輔</div></td><td><div>23</div></td><td><button class="remove-btn">x</button></td></tr>
        <tr><td><div>木久</div></td><td><div>なみ</div></td><td><div>28</div></td><td><button class="remove-btn">x</button></td></tr>
        <tr><td><div>大西</div></td><td><div>昌子</div></td><td><div>31</div></td><td><button class="remove-btn">x</button></td></tr>
        <tr><td><div>岩佐</div></td><td><div>純子</div></td><td><div>45</div></td><td><button class="remove-btn">x</button></td></tr>
        <tr><td><div>清水</div></td><td><div>一久</div></td><td><div>19</div></td><td><button class="remove-btn">x</button></td></tr>
        <tr><td><div>山峰</div></td><td><div>行紀</div></td><td><div>63</div></td><td><button class="remove-btn">x</button></td></tr>
      </tbody>
JavaScript(insert-and-delete-row.js)では、ユーザインタフェースの操作から行データの追加・削除・編集を行う。
/*
 *
 * Copyright(c) 2018 NTT Corporation.
 */
/* jshint camelcase: false */
/* eslint camelcase: 0 */
// insert-and-delete-row.js
'use strict';
$(function () {
  // テーブルにtablesorterを適用する。
  var $tablesorter = $('#tablesorter').tablesorter({
    // (1)編集機能の設定
    widgets: ['editable'],
    widgetOptions: {
      // 編集可能なカラムを設定
      editable_columns: [0, 1, 2],
      // 編集後にEnterキー押下で編集完了とする。
      editable_enterToAccept: true,
      // 編集後の自動ソートを無効にする。
      editable_autoResort: false
    }
  });
  // 行の追加ボタンのイベント
  $('#add-btn').on('click', function () {
    var age = parseInt(Math.random() * 100);
    var row = '<tr><td><div>姓</div></td><td><div>名</div></td><td><div>' + age +
          '</div></td><td><button class="remove-btn">X</button></td>';
    var $row = $(row);
    // テーブルに行を追加する。
    $tablesorter.find('tbody').append($row);
    // (2)追加した行をtablesorterに認識させる。
    $tablesorter.trigger('addRows', [$row, false, function () {
      // (3)追加した行を修正可能にする。
      $tablesorter.trigger('refreshWidgets', true);
    }]);
  });
  // 行の削除ボタンのイベント
  $('table').on('click', 'button.remove-btn', function () {
    // テーブルから行の削除
    $(this).closest('tr').remove();
    // (4)データの削除をtablesorterに認識させる。
    $tablesorter.trigger('update');
  });
});
行追加機能は、 データ行のtr要素をテーブルのtbodyに追加した後、ソースコードリスト上の(2)でaddRowsイベントを発生させることで、行が追加されたことをtablesorterに認識させる。
行削除機能は、 テーブルのtbodyから データ行のtrを削除した後、ソースコードリスト上の(4)でupdateイベントを発生させることで、行が削除されたことをtablesorterに認識させる。
データ編集機能は、ソースコードリスト上の(1)・(3)で実現している。
(1)でwidgetsの配列に'editable'を追加することで、データ編集機能の利用が可能になる。また、widgetOptionsで編集時の動作を設定している。
(3)では、後から追加された行も編集可能とするためにrefreshWidgetsイベントを発生させている。ただしこの処理は行追加が終わった後に実行される必要があるため、(2)のコールバック関数として指定している。
Note
tablesorterはデータ編集を実現するために、contentEditable属性を用いているが、Internet ExplorerではcontentEditable属性をtd要素へ設定することができない。
そのため、セルの値をcontentEditable属性を設定できるdiv要素やspan要素内に記述する必要がある。
なお、このサンプルではdiv要素を用いて実装している。
Note
tablesorterでは、データの追加や削除などを行った後、それらの状態変化をtablesorterに認識させる必要がある。その際に発生させるイベント名を以下に示す。
なお、tablesorterの公式リファレンスでは、これらの手動で発生させるイベントのことを method と表現しているため、参照する際は注意すること。本ガイドラインでもこれにならって メソッド と表現する。
tablesorterの主なメソッド¶ メソッド名 説明 addRows 行を追加する際に実行する。tablesorterがキャッシュしているテーブル情報を更新する。 update 行の削除する際に実行する。tablesorterがキャッシュしているテーブル情報を更新する。 refreshWidgets 設定されているウィジェットを削除し、再設定する際に実行する。 
4.3. 行の並び替え¶
4.3.1. 概要¶
ここでは、SlickGridを用いて、行の並び替えを行う方法を説明する。
| 利用ライブラリ | サンプル | 参考ページ | 
|---|---|---|
| SlickGrid | SlickGridによる行の並び替えサンプル | SlickGrid Wiki | 
4.3.2. 利用方法¶
行の並び替えをするためには、SlickGridに関連付けられたデータ配列の中身を並び替えてテーブルを再描画すればよい。 概念的にはシンプルだが、複数行の一括並び替えなどを実現する場合は、並び替えの処理に複雑な実装が必要となる。
この例では、次のユーザインタフェースでの実現方法を説明する。
- 行ドラッグアンドドロップ操作による並び替えができる
 - Ctrl または Shift クリックで複数行を選択して一括で並び替えができる
 
SlickGrid基本構成サンプルで示したHTMLに加え、次のモジュールを読み込む。それ以外は同様なので省略する。
| モジュール名 | 用途 | 
|---|---|
| slick.editors.js | 行のドラッグアンドドロップ操作ができるユーザインタフェースを提供する | 
| slick.rowselectionmodel.js | 行選択機能を提供する | 
JavaScript(move-row.js)では、ユーザインタフェースの操作によって発生するイベントを監視し、行データの並び替えとテーブルの再描画を行う。
/*
 *
 * Copyright(c) 2018 NTT Corporation.
 */
// move-row.js
'use strict';
(function () {
  // SlickGridのカラム定義
  var columns = [
    // (1) 行ドラッグアンドドロップ機能を有効化するため、すべてのカラムに `behavior: 'selectAndMove'` を設定する。
    {id: 'title', name: 'Title', behavior: 'selectAndMove', field: 'title'},
    {id: 'duration', name: 'Duration', behavior: 'selectAndMove', field: 'duration'},
    {id: '%', name: '% Complete', behavior: 'selectAndMove', field: 'percentComplete'},
    {id: 'start', name: 'Start', behavior: 'selectAndMove', field: 'start'},
    {id: 'finish', name: 'Finish', behavior: 'selectAndMove', field: 'finish'},
    {id: 'effort-driven', name: 'Effort Driven', behavior: 'selectAndMove', field: 'effortDriven'}
  ];
  // SlickGridの動作オプション
  var options = {
    // 何も指定しない。
  };
  // 1000件のサンプルデータ作成
  var data = [];
  for (var i = 0; i < 1000; i++) {
    data[i] = {
      title: 'タスク ' + i,
      duration: '5 days',
      percentComplete: Math.round(Math.random() * 100),
      start: '01/01/2009',
      finish: '01/05/2009',
      effortDriven: (i % 5 === 0) ? 'true' : 'false'
    };
  }
  // 画面初期化処理
  $(function () {
    // SlickGridテーブルを作成
    var grid = new Slick.Grid('#myGrid', data, columns, options);
    // (2) 行選択機能のプラグインを追加
    grid.setSelectionModel(new Slick.RowSelectionModel());
    // (3) 行ドラッグアンドドロップ移動機能のプラグインを追加
    var moveRowsPlugin = new Slick.RowMoveManager();
    grid.registerPlugin(moveRowsPlugin);
    // (4) 行ドラッグアンドドロップイベントハンドラ
    //     移動操作を受けた行を、移動先の位置に挿入して並び替える処理を実装する。
    moveRowsPlugin.onMoveRows.subscribe(function (e, args) {
      var i,
          // 移動操作を受けた行のインデックス番号
          rows = args.rows,
          // 移動先の行のインデックス番号
          insertBefore = args.insertBefore;
      var left = data.slice(0, insertBefore);
      var right = data.slice(insertBefore, data.length);
      // 複数行選択時の選択順を、行のインデックス番号の小さい順でソート
      var extractedRows = [];
      rows.sort(function (a, b) { return a - b; });
      for (i = 0; i < rows.length; i++) {
        extractedRows.push(data[rows[i]]);
      }
      // データ全体のソート
      rows.reverse();
      for (i = 0; i < rows.length; i++) {
        var row = rows[i];
        if (row < insertBefore) {
          left.splice(row, 1);
        } else {
          right.splice(row - insertBefore, 1);
        }
      }
      data = left.concat(extractedRows.concat(right));
      // 選択状態を復元
      var selectedRows = [];
      for (i = 0; i < rows.length; i++) {
        selectedRows.push(left.length + i);
      }
      // データと画面を更新する。
      grid.resetActiveCell();
      grid.setData(data);
      grid.setSelectedRows(selectedRows);
      grid.render();
      grid.invalidate();
    });
  });
}());
行のドラッグアンドドロップ操作は、ソースコードリスト上の(1)・(3)・(4)で実現している。
(3)でドラッグアンドドロップ操作を実現するプラグインをテーブルにセットした上で、(1)のカラム定義で
behaviorプロパティに'selectAndMove'を設定する。
これによりドロップアンドドロップ操作が可能となり、また操作後にはonMoveRowsイベントが発生するようになる。
このイベントを(4)で監視し、コールバック関数内でデータの並び替えと再描画を行っている。
複数行の一括並び替え機能は、(2)・(4)で実現している。 (2)では行選択機能を提供するプラグインをテーブルにセットしている。 (4)ではデータ配列を並び替えてテーブルを再描画している。 並び替えの処理は、複数行の一括の並び替えなど、様々な操作条件に対応するため複雑な実装になっているが、 SlickGridの使用方法の説明という目的から外れるため、この部分の説明は割愛する。
Note
このサンプルでは、複数行を選択して一括で並び替えを行う操作(1行目と4行目を選択し、2行目に移動するなど)を実現しているが、並び替えのロジックはライブラリから提供されていない。ライブラリが提供しているのは、「複数行の選択状態を管理する機能」と「ドラッグアンドドロップ操作によりイベントを発生させる機能」であり、これらの機能と独自実装した並び替えのロジックを組み合せて実現している。
この他にも、SlickGridはロジックの独自実装に活用できるインタフェースや独自イベントを提供していることから、フレームワークとしての性質が強い。
4.4. ヘッダを固定したデータ行のスクロール¶
4.4.1. 概要¶
ここでは、SlickGrid、tablesorterを用いてヘッダを固定してデータ行をスクロールする実装方法を説明する。
| 利用ライブラリ | サンプル | 参考ページ | 
|---|---|---|
| SlickGrid | SlickGridによるヘッダを固定したデータ行のスクロールサンプル | SlickGrid Wiki | 
| tablesorter | tablesorterによるヘッダを固定したデータ行のスクロールサンプル | jQuery plugin: Tablesorter 2.0 - Scroller Widget | 
4.4.2. 利用方法¶
4.4.2.1. SlickGridによるヘッダを固定したデータ行のスクロール¶
SlickGridは標準でヘッダが固定されるため、特別な考慮は不要である。よってSlickGrid基本構成サンプルを参照すること。
4.4.2.2. tablesorterによるヘッダを固定したデータ行のスクロール¶
tablesorter基本構成サンプルで示したHTMLに加え、次のモジュールを読み込む。それ以外は同様なので省略する。
| モジュール名 | 用途 | 
|---|---|
| widget-scroller.js | ヘッダ固定機能を提供する。 | 
JavaScript(fixed-header.js)では、テーブル要素に対しtablesorterメソッドを実行する。ヘッダのthead要素を固定するためにはtablesorterメソッドのオプションであるwidgets配列に'scroller'を追加する。
/*
 *
 * Copyright(c) 2018 NTT Corporation.
 */
/* jshint camelcase: false */
/* eslint camelcase: 0 */
// fixed-header.js
'use strict';
$(function () {
  // ヘッダを固定するテーブルに対し、tablesorterメソッドを実行する。
  // その際、widgetsプロパティに['scroller']を設定する。
  $('#tablesorter').tablesorter({
    widgets: ['scroller'],
    widgetOptions: {
      // テーブルの髙さの指定
      scroller_height : 200
    }
  });
});
widgetOptionsプロパティでテーブル要素の高さを設定できるscroller_heightなどオプションが提供されている。これらの詳細について知りたい場合は、 tablesorterの公式ウェブサイトのリファレンス を参照すること。
4.5. ページネーション¶
4.5.1. 概要¶
ここでは、SlickGridおよびtablesorterを用いて、テーブル表示データのページネーションを行う方法を説明する。
| 利用ライブラリ | サンプル | 参考ページ | 
|---|---|---|
| SlickGrid | SlickGridによるページネーションサンプル | SlickGrid Wiki | 
| tablesorter | tablesorterによるページネーションサンプル | jQuery plugin: Tablesorter 2.0 - Pager plugin | 
4.5.2. 利用方法¶
4.5.2.1. SlickGridによるページネーション¶
SlickGrid基本構成サンプルで示したHTMLに加え、次のモジュールを読み込む。
| モジュール名 | 用途 | 
|---|---|
| slick.dataview.js | データの表示条件を詳細に設定するための部品DataViewを提供する | 
| slick.pager.js | ページネーション操作を行うためのコントロール部品を提供する | 
また、ページネーションコントロールを表示する領域を追加する。 ただし、ページネーションコントロールの中身はライブラリが自動生成するため、HTMLにマークアップする必要はない。
以下に該当個所を抜粋する。それ以外は SlickGrid基本構成サンプル で説明した基本構成と同じなので省略する。
<div id="border">
  <!-- SlickGridテーブルの表示領域となる要素 -->
  <div id="myGrid" class="grid" style="grid"></div>
  <!-- SlickGridページネーションコントロールの表示領域となる要素 -->
  <div id="pager"></div>
</div>
JavaScript(pagination.js)では、ページ切り替え操作によって発生するイベントを監視し、データの表示範囲・件数を切り替えている。
/*
 *
 * Copyright(c) 2018 NTT Corporation.
 */
// pagination.js
'use strict';
(function () {
  // SlickGridカラム設定
  var columns = [
    {id: 'title', name: 'Title', field: 'title'},
    {id: 'duration', name: 'Duration', field: 'duration'},
    {id: '%', name: '% Complete', field: 'percentComplete'},
    {id: 'start', name: 'Start', field: 'start'},
    {id: 'finish', name: 'Finish', field: 'finish'},
    {id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven'}
  ];
  // SlickGrid動作オプション
  var options = {
    // 何も指定しない。
  };
  // データ作成部
  var data = [];
  for (var i = 0; i < 500; i++) {
    data[i] = {
      // DataViewを使用する際はidの設定が必須なことに注意
      id: 'id_' + i,
      title: 'タスク ' + i,
      duration: '5 days',
      percentComplete: Math.round(Math.random() * 100),
      start: '01/01/2009',
      finish: '01/05/2009',
      effortDriven: (i % 5 === 0) ? 'true' : 'false'
    };
  }
  $(function () {
    // (1) DataViewオブジェクトを生成
    var dataView = new Slick.Data.DataView();
    dataView.beginUpdate();
    dataView.setItems(data);
    dataView.endUpdate();
    // SlickGridテーブルを作成
    // コンストラクタの引数には `data` の代わりに `dataView` を渡す。
    var grid = new Slick.Grid('#myGrid', dataView, columns, options);
    // (2) ページネーション機能のプラグインを追加
    var $pager = $('#pager');
    new Slick.Controls.Pager(dataView, grid, $pager);
    // (3) 表示行数が変更された際のイベントハンドラ
    dataView.onRowCountChanged.subscribe(function () {
      grid.updateRowCount();
      grid.render();
    });
    // (4) 表示行が変更された際のイベントハンドラ
    dataView.onRowsChanged.subscribe(function (e, args) {
      grid.invalidateRows(args.rows);
      grid.render();
    });
    // 25件ごと表示する(ドキュメントには載っていないAPIのため注意)。
    dataView.setPagingOptions({pageSize: 25});
    // ページネーションパネルを表示状態にする。
    $pager.find('.slick-pager-settings-expanded').toggle(true);
  });
}());
ソースコードリスト上の(1)で、データの表示条件を詳細に設定するための部品DataViewオブジェクトを生成し、
これに実際のデータdataをセットしている。DataViewは ページネーションコントロールを作成する
Slick.Controls.Pagerコンストラクタを実行するために必要になる。
(2)ではSlick.Controls.Pagerコンストラクタを次のシグネチャに従って実行する。
- 
Slick.Controls.Pager(dataView, grid, $container) Arguments: - dataView (DataView) – テーブルのデータ表示に用いられている
DataViewオブジェクト - grid (SlickGrid) – SlickGridテーブルオブジェクト
 - $container (jQuery) – ページネーションコントロールを表示する要素を選択したjQueryオブジェクト
 
- dataView (DataView) – テーブルのデータ表示に用いられている
 
これにより、ページ切り替え操作によってdataView上で次のイベントが発生するようになる。
| イベント名 | 発生契機 | 
|---|---|
onRowCountChanged | 
ページネーションコントロールでデータ表示行数が変更された際 | 
onRowsChanged | 
ページネーションコントロールでデータ表示行数が変更された際、またはページ送りされた際 | 
これらのイベントを(3)および(4)で監視し、それぞれテーブル表示を更新している。
Note
ページ切り替えのたびにリクエストする場合には、onRowsChangedイベントのタイミングでAjaxリクエストを実行し、レスポンスで得たデータをdataView.setItemsメソッドを用いてセットすればよい。
Note
jQuery v.3.2.0以降の バグ により、ページネーションのAutoを使用する際、表示行数が異なる可能性がある。 jQuery内部で、width()かheight()を取得できない場合、offsetHeight/Widthを設定することで回避はしているものの、 実際に取得できる値は同じではなく、offsetHeightはボーダーを含む高さであるため、 Auto選択時に表示される行数が表示領域に対して異なるという事象が発生します。
4.5.2.2. TableSorterを用いた表示データのページネーション¶
この例では、次のユーザインタフェースを設けることで実現する方法を説明する。
| 表示設定の変更 | 1ページに表示するデータ数の変更、ページ数の変更する部品を設ける。 | 
|---|---|
| ページの移動 | ページを移動するボタンを設ける。 | 
図: ページネーションのユーザインタフェース例
tablesorter基本構成サンプルで示したHTMLに加え、次のモジュールを読み込む。
| モジュール名 | 用途 | 
|---|---|
| jquery.tablesorter.pager.js | ページネーション機能を提供する。 | 
また、ページネーションコントロールを追加する。
以下に該当個所を抜粋する。それ以外はtablesorter基本構成サンプルで説明した基本構成と同じなので省略する。
    <!-- ページネーションコントロール -->
    <div id="pager" class="pager">
      <form>
        <label>表示件数 :
          <!-- 表示件数部品 -->
          <select class="pagesize">
            <option selected="selected" value="10">10</option>
            <option value="20">20</option>
            <option value="30">30</option>
            <option value="40">40</option>
          </select>
        </label>
        <label>ページ数 :
          <!-- ページ選択部品 -->
          <select class="gotoPage">
          </select>
        </label>
        <br/>
        <br/>
        <!-- ページ移動部品 -->
        <img src="../lib/vendor/jquery.tablesorter/2.30.7/addons/pager/icons/first.png" class="first" alt="first"/>
        <img src="../lib/vendor/jquery.tablesorter/2.30.7/addons/pager/icons/prev.png" class="prev" alt="prev"/>
        <span class="pagedisplay"></span>
        <img src="../lib/vendor/jquery.tablesorter/2.30.7/addons/pager/icons/next.png" class="next" alt="next"/>
        <img src="../lib/vendor/jquery.tablesorter/2.30.7/addons/pager/icons/last.png" class="last" alt="last"/>
      </form>
ページネーションコントロールの表示件数部品、ページ数選択部品は、次のクラスを持つselect要素で構成する。
| クラス | 備考 | 
|---|---|
pagesize | 
1ページあたりの表示件数を制御する。 | 
gotoPage | 
ページを制御する。 | 
表示件数部品はoption要素で選択する値を明示する必要があるが、ページ選択部品の値は自動的に計算される。
ページネーションコントロールのページ移動部品は、次のクラスを持つ要素で構成する。
| クラス | 備考 | 
|---|---|
first | 
最初のページに移動するための要素に付与する。 | 
prev | 
1つ前のページに移動するための要素に付与する。 | 
pagedisplay | 
現在のページ数を表示する要素に付与する。 | 
next | 
1つ後のページに移動するための要素に付与する。 | 
last | 
最後のページに移動するための要素に付与する。 | 
JavaScript(pagination.js)では、次の処理を行う。
- ページネーションを適用するテーブル要素をjQueryでセレクトし、
tablesorterメソッドを実行する。 tablesorterPagerメソッドを実行する。その際containerプロパティに、ページネーションコントロールを表示する要素を選択したjQueryオブジェクトを指定する。
/*
 *
 * Copyright(c) 2018 NTT Corporation.
 */
// pagination.js
'use strict';
$(function () {
  // テーブルにtablesorterを適用する。
  var $tablesorter = $('#tablesorter').tablesorter();
  // テーブルにページネーションを適用する。
  $tablesorter.tablesorterPager({
    // ページネーションコントロールに対し設定する。
    container: $('#pager')
  });
});
tablesorterPagerメソッドで設定できるプロパティは他にも提供されている。これらの詳細について知りたい場合は、 tablesorterの公式ウェブサイトのリファレンス を参照すること。
Note
ページ切り替え時にサーバから情報を取得することもできるが、その場合、クライアントでのソートができなくなるため、サーバサイドでソートを実装する必要がある。詳細は、 tablesorterの公式ウェブサイトのリファレンスを参照すること。
4.6. ソート¶
4.6.1. 概要¶
ここでは、SlickGridおよびtablesorterを用いて、テーブルのデータのソートを行う方法を説明する。
| 利用ライブラリ | サンプル | 参考ページ | 
|---|---|---|
| SlickGrid | SlickGridによるソートサンプル | SlickGrid Wiki | 
| tablesorter | tablesorterによるソートサンプル | jQuery plugin: Tablesorter 2.0 - Scroller Widget | 
4.6.2. 利用方法¶
4.6.2.1. SlickGridによるソート¶
HTMLはSlickGrid基本構成サンプルで説明した基本構成と同様なので、省略する。
JavaScript(sort.js)では、ヘッダのクリック操作からソートイベントを発生させ、ソート処理を実行する。
/*
 *
 * Copyright(c) 2018 NTT Corporation.
 */
// sort.js
'use strict';
(function () {
  var i, len;
  // SlickGridのカラム定義
  var columns = [
    // (1) ソート機能を有効化するカラム定義に `sortable: true` を設定する。
    {id: 'id', name: 'ID', field: 'id', sortable: true},
    {id: 'value1', name: 'value 1', field: 'value1', sortable: true},
    {id: 'value2', name: 'value 2', field: 'value2', sortable: true},
    {id: 'value3', name: 'value 3', field: 'value3', sortable: true}
  ];
  // SlickGridの動作オプション
  var options = {
    // 何も指定しない。
  };
  // 1000件のサンプルデータ作成
  var data = [];
  for (i = 0; i < 1000; i++) {
    data[i] = {
      id: 'タスク' + i,
      value1: Math.round(Math.random() * 100),
      value2: Math.round(Math.random() * 1000),
      value3: Math.round(Math.random() * 10000)
    };
  }
  // ソート関数を定義
  // 文字列の場合は文字列比較を、数値の場合は数値比較をした結果でソートする。
  function sortfn(o1, o2) {
    if (o1[column.field] > o2[column.field]) {
      return 1;
    } else if (o1[column.field] < o2[column.field]) {
      return -1;
    }
    return 0;
  }
  // (2) `data` を基にソート用インデックス `indicies` を作成
  //     データ表示時はこのインデックスを経由してデータを引き当てるようにしている。
  var indices = {};
  var column;
  for (i = 0, len = columns.length; i < len; i++) {
    column = columns[i];
    indices[column.id] = [];
    data.sort(sortfn);
    for (var j = 0; j < data.length; j++) {
      indices[column.id][j] = data[j];
    }
  }
  // 画面初期化処理
  $(function () {
    // ソート設定を保持する変数
    var isAsc = true;                  // 初期設定は昇順
    var currentSortCol = { id: 'id'};  // 初期設定は id カラムでソート
    // (3) データ引き当て関数。`Slick.Grid` コンストラクタの引数で使用
    //     ソート用インデックス `indices` を経由して表示データを引き当てる。
    function getItem(index) {
      var newIndex = isAsc ? index : data.length - index - 1;
      return indices[currentSortCol.id][newIndex];
    }
    // データ長取得関数。`Slick.Grid` コンストラクタの引数で使用
    // テーブルに表示するデータの長さは変更しないため、 ``data`` の長さを返す。
    function getLength() {
      return data.length;
    }
    // (4) SlickGridテーブルを作成
    //     第2引数に配列を指定する代わりに、 `getLength` および `getItem` 関数を持つオブジェクトを指定して
    //     ソート設定に基づきデータの表示を行う。
    var grid = new Slick.Grid('#myGrid', {getLength: getLength, getItem: getItem}, columns, options);
    // (5) ソートイベントハンドラ
    grid.onSort.subscribe(function (e, args) {
      // ソート設定を書き換える。
      isAsc = args.sortAsc;
      currentSortCol = args.sortCol;
      // テーブル全体を再描画
      grid.invalidateAllRows();
      grid.render();
    });
  });
}());
ソート機能の実現は、ユーザインタフェース部とデータ表示ルール変更部から成り立つ。
ユーザインタフェース部は、ソースコードリスト上の(1)・(5)で実現している。
(1)でカラム定義のオプションsortableをtrueにすることで、ヘッダがクリックされた際にスタイルの変更や
onSortイベントが発生するようになる。このイベントを(5)で監視し、クリックされたヘッダのカラムを基に
ソート設定を書き換え、テーブル全体を再描画している。
データ表示ルール変更部は、(2)・(3)・(4)で実現している。
(4)で示すように、Slick.Gridコンストラクタの第2引数に、データの配列の代わりにgetLengthとgetItem関数を持つオブジェクトを指定している。こうすることで、テーブルの各行にはgetItem関数の返すデータが表示されるようになる。
getItem関数は(3)で定義している。引数indexは表示行のインデックス番号を受け取るので、これを用いて、
あらかじめ(2)で作成しておいたインデックスからソート設定に基づいてデータを引き当てている。
Note
この例では、ソートのためのインデックス作成とデータ引き当てについて複雑な実装を行っている。 これはパフォーマンスを考慮したためである。
最もシンプルな実装は、onSortイベントハンドラ内で、 クリックされたカラムの値をもとにdata配列を
逆順ソートすることである。この実装の場合、データ長が数千件ならよいが、数万件以上に及んだ場合には
ソートを実行する度に大きなオーバーヘッドが発生する。
SlickGridの主要な用途の一つに大量データ表示が挙げられるため、ここでは大量データ表示に耐えうる ソートの実装を例示した。
4.6.2.2. tablesorterによるソート¶
tablesorterは標準でデータのソートが可能なため、特別な考慮は不要である。よって tablesorter基本構成サンプル を参照すること。
なお、tablesorterは、デフォルトの設定で数値か文字かを自動的に判別しソートできる。 また、複合キーによるソートが可能であり、Shiftキーを押した状態でのソートの操作、またはJavaScriptの実装により、実現できる。 詳細はtablesorterの公式ウェブサイトのリファレンスを参照すること。
Note
標準で提供されているソート機能を無効化したい場合は、無効化したいカラムのth要素に対して、data-sorter=falseの属性を設定する。
4.7. カラムの並び替え¶
SlickGridは標準でカラムの並び替えができるため、特別な考慮は不要である。よってSlickGrid基本構成サンプルを参照すること。
Note
標準で提供されているカラムの並び替え機能を無効化するためには、動作オプションenableColumnReorderをfalseに設定する。
4.8. コピーアンドペースト編集¶
4.8.1. 概要¶
ここでは、SlickGridを用いて、テーブルのデータのコピーアンドペーストによる編集を行う方法を説明する。
| 利用ライブラリ | サンプル | 参考ページ | 
|---|---|---|
| SlickGrid | SlickGridによるコピーアンドペースト編集サンプル | SlickGrid Wiki | 
4.8.2. 利用方法¶
SlickGrid基本構成サンプルで示したHTMLに加え、次のモジュールを読み込む。それ以外は同様なので省略する。
| モジュール名 | 用途 | 
|---|---|
| slick.cellcopymanager.js | コピーアンドペースト機能を提供する | 
| slick.cellrangeselector.js | セル選択機能を提供する | 
| slick.cellrangedecorator.js | セル選択機能を提供する | 
| slick.cellselectionmodel.js | セル選択機能を提供する | 
JavaScript(copy-and-paste.js)では、コピーアンドペースト操作によって発生するイベントを監視し、データのコピーとテーブルの再描画を行う。
/*
 *
 * Copyright(c) 2018 NTT Corporation.
 */
// copy-and-paste.js
'use strict';
(function () {
  // SlickGridのカラム定義
  var columns = [
    {id: 'num', name: '', field: 'id', width: 30},
    {id: 'title', name: 'Title', field: 'title'},
    {id: 'duration', name: 'Duration', field: 'duration'},
    {id: '%', name: '% Complete', field: 'percentComplete'},
    {id: 'start', name: 'Start', field: 'start'},
    {id: 'finish', name: 'Finish', field: 'finish'},
    {id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven'}
  ];
  // SlickGrid動作オプション
  var options = {
    // 何も指定しない。
  };
  // 1000件のサンプルデータ作成
  var data = [];
  for (var i = 0; i < 1000; i++) {
    data[i] = {
      id: i,
      title: 'タスク ' + i,
      duration: '5 days',
      percentComplete: Math.round(Math.random() * 100),
      start: '01/01/2009',
      finish: '01/05/2009',
      effortDriven: (i % 5 === 0) ? 'true' : 'false'
    };
  }
  // 画面初期化処理
  $(function () {
    // SlickGridテーブルを作成
    var grid = new Slick.Grid('#myGrid', data, columns, options);
    // (1) セル選択機能のプラグインを追加
    var cellSelectionModel = new Slick.CellSelectionModel();
    grid.setSelectionModel(cellSelectionModel);
    // (2) コピーアンドペースト機能のプラグインを追加
    var copyManager = new Slick.CellCopyManager();
    grid.registerPlugin(copyManager);
    // (3) ペーストイベントハンドラ
    copyManager.onPasteCells.subscribe(function (e, args) {
      var from = args.from[0];
      var to = args.to[0];
      var i, I, j, J;
      // コピー元のデータを保存するための配列
      var src = [];
      // コピー元のデータを保存
      for (i = 0, I = from.toRow - from.fromRow; i <= I; i++) {
        src[i] = [];
        for (j = 0, J = from.toCell - from.fromCell; j <= J; j++) {
          src[i][j] = data[from.fromRow + i][columns[from.fromCell + j].field];
        }
      }
      // ペースト先にコピーしたデータを上書き
      for (i = 0, I = src.length; i < I && (i + to.toRow) < data.length; i++) {
        for (j = 0, J = src[i].length; j < J && (j + to.toCell) < columns.length; j++) {
          data[to.fromRow + i][columns[to.fromCell + j].field] = src[i][j];
          grid.invalidateRow(to.fromRow + i);
        }
      }
      // ペースト先を選択状態にする。
      cellSelectionModel.setSelectedRanges([{
        fromRow: to.fromRow,
        fromCell: to.fromCell,
        toRow: to.fromRow + i - 1,
        toCell: to.fromCell + j - 1
      }]);
      grid.render();
    });
  });
}());
ソースコードリスト上の(1)で、セル選択機能を提供するプラグインを設定している。また(2)でコピーアンドペースト機能を提供するプラグインを設定している。これによってコピーアンドペースト操作によってonPasteCellsイベントが発生するようになるため、これを(3)で監視し、データのコピー後、テーブルの表示を更新している。
4.9. テーブルのスクロールによる非同期データ取得¶
4.9.1. 概要¶
ここでは、SlickGridを用いて非同期通信でデータを取得し、テーブルに表示する方法を紹介する。
検索画面などで大量データをテーブルに表示する場合、全データを1度に表示するとクライアントやサーバに負荷が掛かる。そのため、初期表示に必要な分だけデータを取得し、以降はユーザー操作に合わせて非同期通信で取得するのが望ましい。
上記を実現するには、スクロール時に非同期通信でデータを取得し、SlickGridに関連付けられたデータ配列に追加してテーブルを再描画すればよい。概念的にはシンプルだが、非同期通信が頻発しないようある程度の件数をまとめて取得するなど、独自に作りこむ必要がある。
| 利用ライブラリ | サンプル | 参考ページ | 
|---|---|---|
| SlickGrid | SlickGridによる非同期データ取得サンプル | SlickGrid Wiki | 
サンプルの動作イメージを示す。 下図は、初期表示時のテーブルを示している。
図: 初期表示時の表示範囲と取得範囲
前提として、本例ではテーブルの領域を以下に分類する。
| 領域 | 説明 | 
|---|---|
| 表示範囲 | テーブルのうち、画面に表示される範囲を示す。本例では20件とする。 | 
| 取得範囲 | 1度の非同期通信で取得する範囲を示す。本例では50件とする。 | 
初期表示時は、取得範囲のデータを非同期通信で取得し、表示範囲のデータを画面に表示する。
次に、スクロール後のイメージを示す。
図: スクロール後の表示範囲と取得範囲
スクロールによって表示範囲が取得範囲からはみ出している。この時点で次の取得範囲分のデータを非同期通信で取得する。
Note
表示範囲の件数はテーブルの高さに依存する。高さを広げる場合、初期表示時にテーブルが空行にならないよう取得範囲を調整すること。
また、取得範囲の件数が表示範囲に近いほど、非同期通信の頻度が高まる。システムの要件などを考慮し、取得範囲の件数を調整すること。
4.9.2. 利用方法¶
SlickGridを利用した非同期データ取得の実装例を紹介する。
SlickGrid基本構成サンプルに示したHTMLに加え、独自に実装した次のモジュールを読み込む。それ以外は同様なので省略する。
JavaScriptは以下を実装する。
| モジュール名 | 用途 | 
|---|---|
| slickgrid-with-ajax.js | スクロールを監視し、非同期通信を行う関数を実行する。また、テーブルを描画する。 | 
| slickgrid-remotemodel.js | 非同期通信でデータを取得する。 | 
各JavaScriptの実装を説明する。
slickgrid-with-ajax.jsの実装を以下に示す。 なお、SlickGridのカラム定義、SlickGrid動作オプションは SlickGrid基本構成サンプルのJavaScript(js/default.js)と同一であるため、省略する。
// slickgrid-with-ajax.js
'use strict';
(function () {
  $(function () {
    // SlickGridのカラム定義
    /* omitted */
    // SlickGrid動作オプション
    /* omitted */
    var grid;
    var loadingIndicator = null;
    // テーブルデータを非同期で取得するオブジェクト生成
    var loader = new Slickgrid.Data.RemoteModel();
    // 画面初期化処理
    $(function () {
      // SlickGridテーブルを作成
      grid = new Slick.Grid('#myGrid', loader.data, columns, options);
      // スクロール時に起動するイベント
      grid.onViewportChanged.subscribe(function () {
        // 開始位置・終了位置取得
        var vp = grid.getViewport();
        // データ取得関数実行
        loader.ensureData(vp.top, vp.bottom);
      });
      // ローディング中に起動するイベント
      loader.onDataLoading.subscribe(function () {
        // データ取得中であることを示すメッセージ出力
        if (!loadingIndicator) {
          loadingIndicator = $('<span><label>Buffering...</label></span>')
            .appendTo(document.body);
          // CSSの設定
          /* omitted */
        }
        loadingIndicator.show();
      });
      // ローディング後に起動するイベント
      loader.onDataLoaded.subscribe(function (e, args) {
        // 表示する行のみ有効化
        for (var i = args.from; i <= args.to; i++) {
          grid.invalidateRow(i);
        }
        grid.render();
        loadingIndicator.fadeOut();
      });
      // テーブル表示
      grid.onViewportChanged.notify();
    });
  });
}());
slickgrid-with-ajax.jsには3つのイベントを実装する。各イベントの機能は以下の通り。
| イベント名 | 説明 | 
|---|---|
| onViewportChanged | スクロール時に発生する。テーブルの表示領域をgrid.getViewportで取得する。 | 
| onDataLoading | 非同期通信中に発生する。データ取得中であることを示すインディケータを表示する。 | 
| onDataLoaded | 非同期通信後に発生する。grid.invalidateRowで再描画対象を設定し、grid.renderでテーブルを再描画する。完了後にインディケータを非表示にする。 | 
次に、slickgrid-remotemodel.jsを説明する。 なお、ソースが長いため、途中で区切って解説を加える。
// slickgrid-remotemodel.js
'use strict';
(function () {
  function RemoteModel() {
    // 変数定義
    var PAGESIZE = 50;
    var data = {
      length : 1000
    };
PAGESIZEは取得範囲の件数である。表示範囲の開始位置・終了位置をPAGESIZEで除算することで、取得範囲上の位置(以降、ポインタとする)を得られる。また、非同期通信で取得するデータの件数などにも使用する。
dataはテーブルデータを格納する。
var hRequest = null;
var req = null;
// イベント
var onDataLoading = new Slick.Event();
var onDataLoaded = new Slick.Event();
function init() {
}
// データ取得関数
function ensureData(from, to) {
  // 非同期通信が実行中の場合、直前の処理を中断する
  if (req) {
    req.abort();
    for (var i = req.fromPage; i <= req.toPage; i++) {
      // 処理中のデータを削除する
      data[i * PAGESIZE] = null;
    }
  }
  // 開始位置が0以下の場合は0に補正する
  if (from < 0) {
    from = 0;
  }
  // 終了位置が上限以上の場合は上限値に補正する
  if (data.length > 0) {
    to = Math.min(to, data.length - 1);
  }
  // 開始位置・終了位置からポインタを算出する
  var fromPage = Math.floor(from / PAGESIZE);
  var toPage = Math.floor(to / PAGESIZE);
パラメータのfrom・toには、テーブル表示の開始位置・終了位置が格納されいている。
PAGESIZEで除算することでポインタを取得する。
// ポインタが異なる場合、位置を補正する
while (!(data[fromPage * PAGESIZE] === null || data[fromPage * PAGESIZE] === undefined) &&
  fromPage < toPage) {
  fromPage++;
}
while (!(data[toPage * PAGESIZE] === null || data[toPage * PAGESIZE] === undefined) &&
  fromPage < toPage) {
  toPage--;
}
スクロールによって表示範囲が取得範囲を跨った場合、ポインタを前後に補正する。
例えばテーブルを下方向にスクロールし、表示範囲がテーブルデータの40件目から60件目にある場合、開始位置のポインタは「0」、終了位置のポインタは「1」となる。
上記の場合はfromPageを加算し、後続の判定でポインタ「1」(テーブルデータの50件目から100件目の範囲)にデータが存在するかチェックする。
// 取得範囲のデータが取得済みか判定する
if (fromPage > toPage ||
  (fromPage === toPage && !(data[fromPage * PAGESIZE] === null || data[fromPage *
    PAGESIZE] === undefined))) {
  // テーブルを再描画するイベントを発生させる
  onDataLoaded.notify({
    from : from,
    to : to
  });
  return;
}
データの有無をチェックし、存在する場合はテーブルを再描画する。
// 非同期通信のURLを編集する
var url = 'js/slickgrid-data.json';
非同期通信に使用するURLを編集する。
Note
本例ではurlを固定としている。サーバからデータを取得する場合は、データを絞り込むために開始位置や取得件数などをパラメータとして設定する。以下に例を示す。
var url = "http://your.server.path/scroll?start=" + (fromPage * PAGESIZE) + "&limit=" + PAGESIZE);
  // setTimeoutが実行中の場合、直前の処理を中断する
  if (hRequest !== null) {
    clearTimeout(hRequest);
  }
  // 非同期通信を実行する
  hRequest = setTimeout(function () {
    // データ取得中であることを示すイベントを発生させる
    onDataLoading.notify();
    // 非同期通信処理
    req = $.ajax({
      type : 'GET',
      url : url,
      dataType : 'json'
    }).then(function (data) {
      // データ配列を編集する
      onSuccess(data, fromPage);
    });
    // 中断時にデータを削除するために格納する
    req.fromPage = fromPage;
    req.toPage = toPage;
  }, 100);
}
スクロールはマウスホイールによって連続的に実行される可能性があるため、setTimeoutで一定時間待機後に非同期通信を実行する。
    // データ編集を行う関数
    function onSuccess(resp, fromPage) {
      var from = fromPage * PAGESIZE, to = from + resp.results.length;
      // 取得したデータをdataに格納する
      for (var i = 0; i < resp.results.length; i++) {
        var item = resp.results[i].item;
        data[from + i] = item;
        data[from + i].index = from + i;
      }
      req = null;
      // テーブルを再描画するイベントを発生させる
      onDataLoaded.notify({
        from : from,
        to : to
      });
    }
    init();
    return {
      // プロパティ
      'data' : data,
      // 関数
      'ensureData' : ensureData,
      // イベント
      'onDataLoading' : onDataLoading,
      'onDataLoaded' : onDataLoaded
    };
  }
  // Slick.Data.RemoteModel
  $.extend(true, window, {
    Slick : {
      Data : {
        RemoteModel : RemoteModel
      }
    }
  });
}());
最後に取得したデータをdataに格納し、テーブルを再描画する。
Note
本例ではテーブルの表示件数を1000件に固定している。必要に応じてdata.lengthに任意の数値を設定すること。
Note
本例ではデータの変動(追加・変更・削除)を考慮していない。取得したデータはクライアント側でキャッシュするため、サーバ側でデータが変動すると齟齬が生じる。非同期通信時にクライアント・サーバ間のデータをチェックするなど、変動を前提とした実装を考慮すること。