Grid.jsを使って簡単な表を作る

JavaScriptライブラリの[Grid.js](https://gridjs.io/)を使って簡単な表を作る方法のメモです。
※Grid.js v.5.1.0を使用

サンプルのコードは、[こちらの記事](https://qiita.com/kazhashimoto/items/d91a9683972a6a508050)を書くときに作った５つの表です。
![tables_example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/2eb8ae3b-e3d0-e3bd-9e5a-a1226a149afb.png)
完成したコードはこちら： https://codepen.io/kaz_hashimoto/pen/WNJvOej

まずはGrid.jsを使うためのHTMLファイルの記述。

```html:index.html (1)
<link rel='stylesheet' href='https://unpkg.com/gridjs/dist/theme/mermaid.min.css'>
<script src='https://unpkg.com/gridjs/dist/gridjs.umd.js'></script>
```

表を埋め込む先のdivを用意します。

```html:index.html (2)
<div id="table1" class="table"></div>
<div id="table2" class="table"></div>
<div id="table3" class="table"></div>
<div id="table4" class="table"></div>
<div id="table5" class="table"></div>
```

表のスタイリングに関して、Grid.jsのデフォルト設定から変更したい部分のみCSSファイルに記述します。Grid.jsが定義するclass名には接頭辞`gridjs-`が付いています[^1]。
[^1]: スタイルの設定は[Grid.jsのAPI](https://gridjs.io/docs/config/style)を通じてもある程度可能ですが、それらは要素のインラインstyleに設定されます。今回は一部のセルだけclassを使ってスタイルを変えられるようにしたいので、APIは使わずにCSSファイルに直接記述しました。

```css:style.css (1)
.gridjs-td {
  text-align: right;
  white-space: nowrap;
}

.gridjs-td:first-child,
#table3 .gridjs-td:nth-child(-n+2) {
  text-align: left;
}
```

Grid.jsが生成した表の`<th>`要素には、CSSプロパティの`text-overflow: ellipsis`が設定されているのですが、Firefoxで表示した場合、見出しがセル幅に収まっているにもかかわらず、文字列右側に省略記号"..." が付いてしまいます。この現象はChromeやSafariでは発生しません。

https://twitter.com/kazhashimoto1/status/1576482378314571776

そこで、回避策として`text-overflow: initial`に設定し省略記号を表示させないようにしました。ブラウザー側で見出しをセル幅に収まるように調整するためこれで問題ありません。

```css:style.css (2)
th.gridjs-th .gridjs-th-content {
  text-overflow: initial; /* for Firefox */
}
```

次にJavaScript。説明の流れをわかりやすくするため、本文のコードブロックはサンプル完成版のものとは異なる順序で載せています。

先に５つの表を生成する部分のコードから。表の生成はとても簡単で、`new gridjs.Grid().render()`の引数に表の列とセルのデータを渡すだけでターゲットのdivに`<table>`要素のツリー構造が埋め込まれます。

forループで`new gridjs.Grid().render()`を呼び出し、５つの表を書き出します。処理をループにまとめるため、各表の列とセルのデータはMapオブジェクト`tables`に格納しておき（詳細は省略）、divのidをキーにデータを取り出せるようにしました。

```javascript:app.js (1)
for (const [id, t] of tables) {
  new gridjs.Grid({
    width: '100%',
    columns: t.columns,
    data: t.data
  }).render(document.getElementById(id));
}
```

`gridjs.Grid()`の引数に指定した`width: '100%'`は、`<table>`要素が含まれるコンテナーの幅になります。
※[デフォルトが100%](https://gridjs.io/docs/config/width)なので今回の場合省略可
![dom01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/a8bca596-6b75-55ae-6ccf-d5b48a788273.png)

### セルのデータの定義
表"table1"のセルのデータを定義する配列です。配列の要素が表の１行分のデータです。表"table1"の左から6番目の列については計算式で値を算出するため、この配列の要素には定義しません。

```javascript:app.js (3)
let tab = tables.get('table1');
tab.data = [
  ['Chrome', 1024, 1200, 1024, 1],
  ['Firefox', 1024, 1024, 1024, 1],
  ['Safari', 2560, 1024, 1024, 1],
  ['Edge', 1024, 1200, 1024, 1]
];
```

### 列の定義
表"table1"の列を定義する配列です。

```javascript:app.js (4)
tab.columns = [
  'Browser',
  {id: 1, name: br('screen.width'), attributes: marker},
  {id: 2, name: br('window.innerWidth'), attributes: marker},
  {id: 3, name: br('visualViewport.width'), attributes: marker},
  {id: 4, name: br('visualViewport.scale')},
  {id: 5, name: br('visualViewport.width x scale'),
  formatter: (_, row) => multiply(row.cells[3].data, row.cells[4].data)},
  {id: 6, name: '', width: '100%'}
];
```

**形式1:** 配列の要素を文字列で指定した場合は、そのまま列見出しの文字列になります。（例： `'Browser'`）

**形式2:** 列見出しにHTML要素を含めたい場合は、配列の要素をオブジェクトにして[idとname](https://gridjs.io/docs/config/columns)プロパティに分けて設定します。

```javascript:app.js (4a)
  {id: 4, name: br('visualViewport.scale')},
```

この例では、列見出しに`<br>`タグを挿入するために`br()`関数（後述）を呼び出して戻り値のHTML文字列を`name`プロパティに設定しています。
`id`は列を識別するための任意の文字列です。数値で指定しても文字列として扱われるようです。

**形式3:** 列の幅は[width](https://gridjs.io/docs/config/columns)プロパティで指定します。

各表の右端の列は、コンテナーの幅に合わせて伸縮する空の列にしたいので、`name`の値を空文字列にして`width`に`100%`を設定します。これでセル幅が残りスペースに合わせて調整されます。

```javascript:app.js (4b)
  {id: 6, name: '', width: '100%'}
```

列見出しを文字列で配列要素に直接指定するのと`id`と`name`に分けて指定するのとの違いは、生成された`<table>`要素の構造を見るとわかります。下図の`data-column-id`属性と`div.gridjs-th-content`の内容に注目。
![dom02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/c8f53988-ef77-0800-bc79-febf540fc7bf.png)
**形式4:** セルの値によってセルのスタイルを変えたいときは、[attributes](https://gridjs.io/docs/examples/cell-attributes)プロパティにハンドラの関数を設定します。

この例では、セルの背景を青色で塗りつぶすために`marker()`関数（後述）を呼び出すように設定しています。

```javascript:app.js (4c)
  {id: 1, name: br('screen.width'), attributes: marker},
```

**形式5:** セルに数式を定義し、計算結果をセルの値にするには、[formatter](https://gridjs.io/docs/examples/cell-formatting)プロパティにハンドラの関数を設定します。

この例では、`cells[3]`と`cells[4]`のセルの値を乗算した結果をこのセルの値とするように設定しています。乗算を行う関数は後述の`multiply()`で定義してあります。なお、配列`cells`では同じ行のセルのみ参照できます。

```javascript:app.js (4d)
  {id: 5, name: br('visualViewport.width x scale'),
  formatter: (_, row) => multiply(row.cells[3].data, row.cells[4].data)},
```

### セルの書式設定
`attributes`プロパティや`formatter`プロパティに指定したハンドラの関数です。

最初に関数`br()`。呼ばれると列見出しに`<br>`タグを挿入します。パラメーター`cell`はセルの内容（文字列）です。戻り値はHTML文字列です。HTML文字列への変換は[gridjs.html](https://gridjs.io/docs/examples/html-cells)を呼び出して行います。

今回のサンプルの表では、列見出しに含まれる最初のピリオド"."の前に`<br>`を挿入しています。

```javascript:app.js (5)
function br(cell) {
  cell = cell.replace(/\./, '<br>.');
  return gridjs.html(cell);
}
```

次に関数`marker()`。呼ばれるとセルの値が1024より大きい場合にセルの背景色を設定します。戻り値はCSSのstyleを記述したオブジェクトです。

```javascript:app.js (6)
function marker(cell) {
  let d = +cell;
  if (d > 1024) {
    return {
      'style': {
        'background': '#a5d8ff'
      }
    };
  }
}
```

`marker()`が呼ばれると`<td>`要素のインラインstyleに`background`プロパティが設定されます。
![dom03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/86ce2da3-4915-4810-34d3-633486e1749f.png)
最後に関数`multiply()`。パラメーターとして２つのセルの内容（文字列）を受け取り、それらを数値として乗算した結果の値を四捨五入して返します。

```javascript:app.js (8)
function multiply(c1, c2) {
  if ((c1 == 'n/a') || (c2 == 'n/a')) {
    return 'n/a';
  }
  let v = Number(c1) * Number(c2);
  return Math.round(v);
}
```

今回のサンプルの表には一部のセルに文字列"n/a"を入れてあるので、それらがパラメーターで渡された場合は"n/a"を返します。