Handsontable(+React)でチェックボックスをカスタマイズする方法

簡単にExcelライクなデータグリッドが作成可能な[Handsontable](https://handsontable.com/)には、標準のセルタイプとして"checkbox"があります。
しかし描画されるチェックボックスはブラウザ標準のもので少し味気ないです。特にCSSフレームワーク等を導入していている場合、Handsontableのチェックボックスだけ違和感が出ると思います。

この記事ではチェックボックスの挙動はそのままで、見た目だけカスタマイズする方法を書きます。
例ではReactを使用していますが、拡張機構等は本体機能なのでReactなしでも同様の方法でできます。

# 環境

Handsontable: v6.1.1
React: v16.6

# カスタマイズ前

カスタマイズ前のコードと表示結果は以下のとおりです。

```jsx
const data = [
  { foo: true, bar: true },
  { foo: true, bar: false },
  { foo: false, bar: true },
  { foo: false, bar: false },
]

const columns = [
  { data: "foo", type: "checkbox" },
  { data: "bar", type: "checkbox" },
]

ReactDOM.render(
  <HotTable data={data} columns={columns} rowHeaders={true} colHeaders={["foo", "bar"]} />, 
  document.getElementById("example")
);
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/25588/8e6a615b-408c-b82f-61d8-92d4143d12a7.png)



# カスタマイズ

例として [Material-UI](https://material-ui.com/demos/selection-controls/)のチェックボックスを表示するようにします。

## レンダラの登録

Handsontableではセルのレンダラやエディタを拡張することができます。
レンダラは名称をつけて登録しておき、カラム設定時に名称で指定したり、直接レンダラ関数を渡すことができます。
ここでは汎用的に登録してみます。

```jsx
Handsontable.renderers.registerRenderer("mui.checkbox", function checkboxRenderer(...args) {
  Handsontable.renderers.CheckboxRenderer.apply(this, args);
  const [, td, row, column, , value] = args;

  // data-row, data-colはHandsontableでデータ更新時のキーに使われている
  const cell = (
    <div style={{ textAlign: "center" }}>
      <Checkbox checked={value} inputProps={ { "data-row": row, "data-col": column } } />
    </div>
  );

  td.innerHTML = ReactDOMServer.renderToString(cell);
});
```

`registerRenderer` 関数の第一引数("mui.checkbox")がレンダラ名称です。名称は任意ですが標準のレンダラとバッティングしないように"mui."のように名前空間というかプレフィックスをつけておくといいと思います。

2行目は挙動を標準のチェックボックスと同じにしたいために標準のチェックボックスレンダラを呼んでいます。これにより、スペースキーでチェックON/OFFができたり、セルのコピー＆ペースト等の挙動は同じになります。

8行目あたりでMaterial-UIのチェックボックスを作っています。ポイントは最終的に描画されるものを `<input type="checkbox" data-row="0" data-col="0">`
のようにしなければならないということです。(Material-UIの `inputProps` はinputのプロパティ) 
チェックボックスのクリック時など、Handsontableはこれらのdata属性を使ってデータセットの更新を行っているようです。

12行目あたりで作ったものをHTML文字列にしてtd(セル)の中身にしています。上記はcodepen用のコードなので、実際は `import { renderToString } from "react-dom/server";` の方が一般的だと思います。
ちなみに、 `ReactDOM.render(cell, td)` でも動きますが、セルが更新された際に怒られます。(reactでmountしたものはちゃんとreactのunmountを使えよ、と)

## レンダラの指定

カラムの設定時に上記で登録したレンダラを指定します。

```jsx
const columns = [
  { data: "foo", type: "checkbox", renderer: "mui.checkbox" },
  { data: "bar", type: "checkbox", renderer: "mui.checkbox" },
]
```

# 完成形

```jsx
import Checkbox = "@material-ui/core/Checkbox";

Handsontable.renderers.registerRenderer("mui.checkbox", function checkboxRenderer(...args) {
  Handsontable.renderers.CheckboxRenderer.apply(this, args);
  const [, td, row, column, , value] = args;

  // data-row, data-colはHandsontableでデータ更新時のキーに使われている
  const cell = (
    <div style={{ textAlign: "center" }}>
      <Checkbox checked={value} inputProps={ { "data-row": row, "data-col": column } } />
    </div>
  );

  td.innerHTML = ReactDOMServer.renderToString(cell);
});

const data = [
  { foo: true, bar: true },
  { foo: true, bar: false },
  { foo: false, bar: true },
  { foo: false, bar: false },
]

const columns = [
  { data: "foo", type: "checkbox", renderer: "mui.checkbox" },
  { data: "bar", type: "checkbox", renderer: "mui.checkbox" },
]

ReactDOM.render(
  <HotTable data={data} columns={columns} rowHeaders={true} colHeaders={["foo", "bar"]} />,
  document.getElementById("example")
);
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/25588/9e38552e-1091-7980-650e-320aa9327339.png)