Tabulatorにて、フォーマットしたセルをヘッダーフィルタで絞り込みを実装する

去年までグリッドテーブル表示はw2uiでしたが、tabulatorに完全乗り換えたmajirouです。

[tabulator](http://tabulator.info/) はセルの値に対して、ルックアップするような形でそのセルに表示する値を`formatter`で指定できます。

例えば、`formatter: 'money'`とすれば、セルの値`1000`を、表示上`1,000`と整形してくれます。
規定値以外に関数を定義することができるので、今回はそれを行います。

# 完成

先に出来上がったものをCodePenに記述しました。

<p class="codepen" data-height="265" data-theme-id="0" data-default-tab="js,result" data-user="majirou" data-slug-hash="bJzRVZ" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="tabulator sample">
  <span>See the Pen <a href="https://codepen.io/majirou/pen/bJzRVZ/">
  tabulator sample</a> by majirou (<a href="https://codepen.io/majirou">@majirou</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

# 解説

## データについて

今回の例では、以下のような「ウシ」や「ブタ」などの名前と共に、categoryという値をもつデータを扱います。
categoryには、1～5の数値が入っており、それぞれ該当する名称が別途データで提供されています。

```js
const data = [
 { id: 1, name: 'ウシ' , category: 5},
 { id: 2, name: 'ブタ' , category: 5},
 { id: 3, name: 'トリ' , category: 4},
 { id: 4, name: 'ヒツジ' , category: 5},
 { id: 5, name: 'マグロ' , category: 1}
]

const categoryList = [
 { id: 1, name: '魚類'},
 { id: 2, name: '両生類'},
 { id: 3, name: '爬虫類'},
 { id: 4, name: '鳥類'},
 { id: 5, name: '哺乳類'},
]
```

# tabulator本体

これをtabulatorで表示していきます。
高さやレイアウトを適当に定めて、`headerFilter`にてヘッダーフィルターを有効にします。

```js
const table = new Tabulator( '#table' , {
  height: 300, 
  layout:"fitColumns", 
  data,
  columns: [
    { field: 'name', title:'Name', headeFilter: true } , 
    { field: 'category', title:'Category', headeFilter: true } 
  ]
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136889/60961e87-de68-5edf-bcf1-198d03e3d466.png)

`headerFilter: true`を指定したので、各列のヘッダーにフィルターが付与されたデータテーブルが作成されました。

## セルのフォーマット

このままではカテゴリー列は数値そのままなので、一体何のカテゴリーなのかわかりません。
1なら「魚類」、4なら「鳥類」と表示するために、「該当する値を探し、その名前を返す関数」を`formatter`に定義します。

```diff
  columns: [
    { field: 'name', title:'Name', headeFilter: true } , 
-    { field: 'category', title:'Category', headeFilter: true } 
+    { field: 'category', title:'Category', headerFilter: true, formatter: cell => { 
+      const categoryIndex = Object.keys(categoryList).find(f => { return categoryList[f].id === cell.getValue() })
+      return (categoryList[categoryIndex] != null) ? categoryList[categoryIndex].name : cell.getValue()
+    }}
  ]
```

すると、先ほど数値が表示されていた部分が、該当する「哺乳類」や「鳥類」に変わって表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136889/b267cab6-a69f-a2f6-02d1-281a978da267.png)

## 動作しないヘッダーフィルター

ところが、このヘッダーフィルターにて「哺乳類」と入力しても、期待する絞り込み結果にはなりません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136889/1240b183-071a-108f-aeaa-0d9737097c5a.png)

実際のセル値は、1や4といった値であり、見かけ上だけがフォーマットされ「何類」か表示されているだけだからです。
これを解決するためには、`headerFilterFunc` に入力した値で絞り込める処理関数を定義します。

## ヘッダーフィルター関数のカスタマイズ

公式ドキュメントの[Filter Comparison Types](http://tabulator.info/docs/4.2/filter#header)項を参照すると、以下のようにサンプルがありますので、これを真似て「該当する値を探し、その名前を返す関数」を定義します。

> ```js
function customHeaderFilter(headerValue, rowValue, rowData, filterParams){
    //headerValue - the value of the header filter element
    //rowValue - the value of the column in this row
    //rowData - the data for the row being filtered
    //filterParams - params object passed to the headerFilterFuncParams property
    return rowData.name == filterParams.name && rowValue < headerValue; //must return a boolean, true if it passes the filter.
}
//column definition object in table constructor
{title:"Age", field:"age", headerFilter:"input", headerFilterPlaceholder:"Max Age", headerFilterFunc:customHeaderFilter, headerFilterFuncParams:{name:"bob"}}
```

## ポイント

* 引数 `headerValue`が、ヘッダーフィルターに入力した値
* 引数 `rowValue`が、セルの値

つまり、今回の例でいうと、
* `headerValue` が、入力した 「哺乳類」
* `rowValue`が、1,4,5などの値

となりますので、入力した値にマッチするかを正規表現で判定し、true/falseを返すことで、フィルタリング関数として動作します。


```diff
  columns:[              
    { title:"Name", field:"name" ,headerFilter: true},
    { title:'Category', field: 'category', headerFilter: true, formatter: cell => { 
      const categoryIndex = Object.keys(categoryList).find(f => { return categoryList[f].id === cell.getValue() })
      return (categoryList[categoryIndex] != null) ? categoryList[categoryIndex].name : cell.getValue()
-    }
+    }, headerFilterFunc: (headerValue, rowValue, rowData, filterParams) => {
+      const regexp = new RegExp(headerValue)
+      const categoryIndex = Object.keys(categoryList).find(f => { return categoryList[f].id === rowValue })
+      return regexp.test(categoryList[categoryIndex].name) 
    }}
```

以上で、冒頭の内容が出来上がります。

## セル値の置換について

わざわざ自前で関数を用意しなければ、表示に則したヘッダーフィルターが有効にならないと記載しました。

これに関して、`formatter`で表示上を変えるのではなく、`mutator` を使えばセルの値自体を置換することができます。
従って、`headerFilterFunc`を定義しなくても上述のようなヘッダーフィルターは可能になります。

ただし、表示するだけなら問題ないのですが、例えばその行のデータを使って別途フォームを表示し、サーバーとやりとりするなどが発生しうる場合、
`mutator` を行ってしまうと、値そのものが変わってしまうので、サーバーに送る際に置換した値を元の値に戻す処理などが必要になります。


# まとめ

* `formatter`した値を絞り込むには、`headerFilterFunc`に入力値で絞り込む関数を定義する
* 絞り込むだけなら`mutator`でセル値を変えてしまう方が楽。ただし、データそのものが置き換わるので注意