React + json-server + ag-Grid で色々と試してみた ［ag-Grid応用編］

# はじめに

この記事は、[前回記事](https://qiita.com/potstickers/items/b06e1cf3665aa6844881)、[前々回記事](https://qiita.com/potstickers/items/5ca331b939a8bb2056cf) の続編です。環境周りの情報やモック API の作成方法、ag-Grid の基本的な使い方はそちらに記載していますので、是非そちらも一読ください。

今回は ag-Grid の設定を色々と深堀りしながら、どのようなカスタマイズができるかを見ていきます。ちょっと文章量が多いですが、実用的で役に立つ情報が多いと思うので、くじけずに最後まで付いてきてください。

フロントエンドの構築には React と Material-UI を使っていますが、この記事の目的は ag-Grid の機能や実装方法を紹介することなので、ここでは取り扱いません。また、掲載しているソースコードは、説明を分かりやすくするために直接的な関係がない部分を思い切って省略しているのでご注意ください。

それでは行ってみましょう！

# テーブル編
## ページ機能
![page.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/c0994a4d-4027-76a6-b9ba-21b7aeba7bc6.gif)
テーブルを作ったらまず欲しくなるのがページ機能ですよね！もはや必須機能と言っても過言ではない気がします。この機能は AgGridReact コンポーネントの `pagination` を有効にして `paginationPageSize` に１ページあたりの表示件数を指定するだけで簡単に実現できます。基本機能とはいえ、お手軽ですよね。

さらに、前回記事で触れた Grid API を使うと、１ページあたりの表示件数を動的に変更したり、ページを移動したりといったこともできます。詳しくは [ここ](https://www.ag-grid.com/documentation/react/row-pagination/) を見てみてください。

```jsx
const [gridApi, setGridApi] = useState(null);

const onGridReady = (grid) => {
  setGridApi(grid.api);
};

// 何らかのイベントハンドラから呼び出す
const pageSizeChanged = (pageSize) => {
  gridApi.paginationSetPageSize(pageSize);  // １ページあたりの表示件数を変更
};

<AgGridReact
  pagination={true}
  paginationPageSize={10}
/>
```
補足になりますが、ag-Grid の community 版では大きなデータを取り扱うための方法が２つ提供されています。

* Client-Side Row Model : すべてのデータを一度にグリッドにロードする。（デフォルト）
* Infinite Row Model : ユーザーの要求に応じてデータを取得してグリッドにロードする。グルーピング非対応。

Client-Side Row Model はグリッド生成時に全てのデータをロードする方法なので、ソートやフィルタに ag-Grid の標準機能が利用できます。しかし、大量のデータを扱う場合はクライアント側のリソースを大量に消費するので、パフォーマンス面で不利になります。

一方 Infinite Row Model は現在の画面で表示に必要なデータのみをサーバーから取得して表示する方法なので、サーバー側にソートやフィルタ機能を作り込む必要がありますが、クライアント側のリソース消費を節約できます。手間はかかりますが、特に大きなデータを取り扱う場合にはこちらを使うのが無難そうです。

## 行選択
![select.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/775aa985-66eb-53bc-8098-4f9829e3ef16.gif)
AgGridReact コンポーネントの `rowSelection` を設定するだけで行の選択機能が有効になります。単一行選択だけを許可する場合は `single` 、複数行選択を許容する場合は `multiple` を指定します。

複数行を選択する場合、標準だと Ctrl キーを押しながら行をクリックする必要があるのですが、`rowMultiSelectWithClick` を指定するとマウス操作だけで複数行の選択が可能になります。

選択された行は背景色が変化するので、どの行が選択されているか視覚的に判別できるのですが、列定義に `checkboxSelection`  または `headerCheckboxSelection` を有効すると、選択状況と連動したチェックボックスが表示され、更に見分けやすくなります。

行選択に関する設定も数多くあるので、もっと詳しく知りたい人は [ここ](https://www.ag-grid.com/documentation/react/row-selection/) を見てみてください。

```jsx
// 列定義部
{
  headerName: "Id",
  field: "id",
  checkboxSelection: true,  // 各行にチェックボックスを表示
  headerCheckboxSelection: true, // 各行に加えてヘッダに全選択チェックボックスを表示
}

<AgGridReact
  rowSelection={"multiple"}
  rowMultiSelectWithClick={true}
/>
```

## 複数列ソート
![sort.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/ae75a3b5-cf1e-3700-f28d-c1acda723efc.gif)
複数列でソートしたいという要望はよく聞くと思うのですが、実は列定義の `sortable` を設定すると複数列ソートも有効になっていて、Shift キーを押しながら列ヘッダーをクリックすることで複数条件を指定できます。

何となく「Shift キーは好みじゃないんだよな」という人は AgGridReact コンポーネントの `multiSortKey` に `ctrl` を指定してキーを変更することもできます（残念ながら指定できるのは Ctrl キーのみです…惜しい）。また、`animateRows` を有効にして、並び替わる様子にアニメーション効果を加えることも可能です。

列ソートにもかなり多くの設定があるので、詳しくは [ここ](https://www.ag-grid.com/documentation/react/row-sorting) を見てみてください。

``` jsx
// 列定義部
{
  headerName: "Full Name",
  field: "name",
  sortable: true,
}

<AgGridReact
  multiSortKey={"ctrl"}
  animateRows={true}
/>
```
## 列固定
![pined.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/3dc78027-0bcf-edb5-2a80-c56677f77313.gif)
これも Excel に慣れ親しんだユーザーからよく出る要望ですよね。ag-Gridではテーブルの高さと、表示されるデータの量に応じて行ヘッダが自動的に固定されるので、行方向については特に意識をする必要はないですが、列の固定も簡単にできます。

具体的には列定義で `pinned` に `left` を指定すると左側に、`right` を指定すると右側に列固定できます。メチャクチャ簡単ですね！

```jsx
// 列定義部
{
  headerName: "Id",
  field: "id",
  pinned: "left",  // 列を左に固定
}
```

# セル編
## セルレンダー
![renderer.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/de5cafce-d952-9949-d5e4-c6890856015b.gif)
セルに値をそのまま表示するだけであれば列定義のマッピングだけで事足りるのですが、例えばボタンを配置したり、値によってアイコンを表示したり複雑なことをしたいケースがあると思います。その時に使えるのがセルレンダーという機能です。

これを使うと、関数内で組み立てた HTML でセルの値を表現することが可能になります。この関数は、`ICellRendererParams` という引数を取り、これを通してグリッド上のあらゆるデータにアクセスが可能です。詳細は [このあたり](https://www.ag-grid.com/react-grid/component-cell-renderer/#cell-renderer-component-2) を見てみてください。

手始めにボタンを表示するサンプルから見ていきましょう。引数として `ICellRendererParams` を受け取って、React のコンポーネントを組み立てて返却するだけです。

```jsx
// セルレンダ定義
const buttonRenderer = (gridObject) => {
  return (
    <Grid container alignItems="center">
      <Grid item>
        <Button
          variant="contained"
          color="primary"
          size="small"
          onClick={() => {
            alert(gridObject.data.name);  // 同一行のname値を参照
          }}
        >
          click
        </Button>
      </Grid>
    </Grid>
  );
};

// 列定義
{
  headerName: "Button",
  cellRendererFramework: buttonRenderer,
}
```
![icon.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/7a3958ea-3a51-26d4-1c94-1e8d616fe2df.gif)
次はセルの値に応じてアイコンを表示するサンプルです。ちょっとゴチャゴチャしていますが、基本的な考え方はボタン表示と同じです。

``` jsx
// セルレンダ定義
const rankRenderer = (gridObject) => {
  let rank = parseInt(gridObject.data.rank);  // 同一行のrank値を参照

  return (
    <Grid container alignItems="center">
      {rank <= 10 && <SentimentVerySatisfiedIcon style={{ fill: "#f57c00" }} />}
      {10 < rank && rank <= 20 && (
        <SentimentSatisfiedAltIcon style={{ fill: "#ffa000" }} />
      )}
      {20 < rank && rank <= 30 && (
        <SentimentSatisfiedIcon style={{ fill: "#fbc02d" }} />
      )}
      {30 < rank && <SentimentDissatisfiedIcon style={{ fill: "#bdbdbd" }} />}
      <Typography variant="inherit" style={{ marginLeft: 5 }}>
        {rank}
      </Typography>
    </Grid>
  );
};

// 列定義
{
  headerName: "Rank",
  field: "rank",
  cellRendererFramework: rankRenderer,
}
```
ちなみに `cellRendererSelector` という機能を使えば、条件に応じてセルレンダ自体を切り替えるということもできます。詳しくは [このあたり](https://www.ag-grid.com/react-grid/cell-rendering/#many-renderers-one-column) を見てみてください。

## バリュー・フォーマッタ
![formatter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/7eb4bb82-e116-9c57-61dc-1d243d799b2e.gif)
これは名前の通り、数値の３桁カンマ区切りや、日付の表示形式を調整したりするのに使える機能です。セルレンダーとよく似ていますが、違いは関数で返す値が HTML ではなく、文字列になっていることです。値のフォーマットだけが目的のときは、こちらで十分ですね！

```jsx
const dateFormatter = (gridObject) => {
  // 年月に前ゼロ編集
  return dayjs(gridObject.value, "YYYY/M/D").format("YYYY/MM/DD");
};

// 列定義
{
  headerName: "Create Date",
  field: "create_at",
  valueFormatter: dateFormatter,
}
```

## セルスタイル
![styler.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/7cc12ca3-50ac-05b7-1753-b283008d1cf7.gif)
セルのスタイルを動的に変更する機能で、これも基本的な作りはセルレンダと同じです。スタイルを設定する方法は３つ用意されています。

* CSS オブジェクトを指定する方法
* CSS クラス名を指定する方法
* CSS クラス名を判定するためのルールを指定する方法

今回は CSS オブジェクトを指定するサンプルを掲載しますが、前述の通りセルレンダと同様の作りとなっているので説明は不要でしょう。

```jsx
const categoryCellStyler = (gridObject) => {
  switch (gridObject.value) {
    case "A":
      return { borderLeftWidth: "10px", borderLeftColor: "#66bb6a" };
    case "B":
      return { borderLeftWidth: "10px", borderLeftColor: "#81c784" };
    case "C":
      return { borderLeftWidth: "10px", borderLeftColor: "#a5d6a7" };
    case "D":
      return { borderLeftWidth: "10px", borderLeftColor: "#c8e6c9" };
    default:
      break;
  }
};

// 列定義
{
  headerName: "Category",
  field: "category",
  cellStyle: categoryCellStyler,
}
```
# ソート・フィルタ編
## ソート・コンパレータ
![customsort.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/aa793b8f-c03a-5058-cddc-7fd2b12b2e0c.gif)
自然順ではなく、独自の条件で並び替えを行いたい時に使えるのがソート・コンパレータです。ソート・コンパレータは引数に比較対象行（Ａ、Ｂと呼ぶ）の情報を受け取るので、これを使って並び順の定義を行います。具体的には、行Ａを先に並べたい場合は正の整数、後に並べたい場合は負の整数、同順としたい場合は０を返すように関数を定義するだけです。

```jsx
// ランク順・年齢順にソート
const rankSortComparator = (valueA, valueB, nodeA, nodeB, isInverted) => {
  if (valueA === valueB) {
    if (nodeA.data.age === nodeB.data.age) return 0;
    return nodeA.data.age > nodeB.data.age ? 1 : -1;
  }
  return valueA > valueB ? 1 : -1;
};

// 列定義
{
  headerName: "Rank",
  field: "rank",
  comparator: rankSortComparator,
}
```

## フィルタ・コンパレータ
![filter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/cd0b7e87-dfeb-19ce-79cd-39bbdaf4d46b.gif)
community 版では文字列、数値、日付の列フィルタが使えるのですが、このうち、文字列と日付の列フィルタには独自の比較条件（コンパレータ）を指定することができます。例えば大文字小文字を無視してマッチさせたり、独自の日付フォーマットのデータをマッチさせたりといったことができます。

文字列用と日付用では少し指定する方法が異なるのですが、日付用の場合はセル値がフィルター値より大きい場合は正の整数、小さい場合は負の整数、等しい場合は０を返すようにコンパレータを定義するだけです。ソート・コンパレータとほぼ同じですね。

```jsx
const dateFilterComparator = (filterValue, cellValue) => {
  let filterDate = dayjs(filterValue);
  let cellDate = dayjs(cellValue, "YYYY/M/D").startOf('date'); // 時刻以降の精度を切捨て
  return cellDate.diff(filterDate, "day");
}

// 列定義
{
  headerName: "Create Date",
  field: "create_at",
  filterParams: {
    buttons: ["apply", "reset"], // フィルタパネルに表示するボタンを指定
    comparator: dateFilterComparator,
  }
}
```
# データ編集編
## 変更セル表示
![mod.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/cfca1ab6-9547-9712-9cc5-1c9152408f84.gif)
値が変更されたら、どのセルが変更されたか区別できるようにしたくなるのが人情ですよね。ここでは値が変わったら背景色を変更して区別できるようにしてみたいと思います。

ポイントは AgGridReact コンポーネントの `onCellValueChanged` にコールバック関数を仕込むところで、その中で動的セルスタイルのセットと、フラグ制御、リフレッシュを行うと背景色の変更が実現できます。おそらくソースを見たほうがわかりやすいと思うので下のコードを見てみてください。

``` jsx
// セルが変更された際のフラッシュ色を標準から変更
const style = `
  .ag-theme-alpine .ag-cell-data-changed {
    background-color: #ffcdd2 !important;
  }`;

// 列定義部
{
  headerName: "Full Name",
  field: "name",
  editable: true,
}

const onCellValueChanged = (event) => {
  // 動的セルスタイルをセット（defaultColDefで最初に設定してしまってもいいかも）
  event.colDef.cellStyle = (params) => {
    if (params.data.isEdited) {
      return { "background-color": "#ffebee" };
    }
  };
  // 変更フラグを立てる
  event.data.isEdited = true;
  // 画面を更新するためにリフレッシュ（このタイミングで背景色が変わる）
  event.api.refreshCells({
    force: true,
    columns: [event.column.colDef.field],
    rowNodes: [event.node],
  });
};

<style>{style}</style>  // スタイルを適用する
<AgGridReact
  onCellValueChanged={onCellValueChanged}
  enableCellChangeFlash={true}  // フラッシュを有効化
  cellFlashDelay={100}  // フラッシュ時間
/>
```
おまけ的な要素ですが、AgGridReact コンポーネントの `enableCellChangeFlash` と `cellFlashDelay` を設定することで変更のあったセルをフラッシュさせることができます🤩

また、デフォルトだとフラッシュ色は緑っぽい色なのですが、`.ag-theme-alpine .ag-cell-data-changed` を上書きしてやることで好きな色に変更することができます。

## 変更行管理
![edit.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/b1cc5825-94c9-dc79-cc47-38f288f0fa5b.gif)
列定義で `editable` を有効にするとセル値の編集が可能になりますが、値が変更されたらそれをサーバーに保存したり、普通は何らかの処理をしますよね。実現方法は色々ありますが、一例として変更箇所を一元管理して、まとめて処理する方法を紹介します。

これも前述の「変更セル表示」と同様に `onCellValueChanged` を使うのですが、その関数で変更があった行の `rowId` を管理し、あとは煮るなり焼くなりご自由にといった感じです。

``` jsx

// 列定義部
{
  headerName: "Full Name",
  field: "name",
  editable: true,
}

// 変更行セット
const changeset = useRef(new Set());

// セル変更のイベントハンドラで変更行セットに行IDを追加する
const onCellValueChanged = (event) => {
  changeset.current.add(event.node.id);
};

const saveClicked = () => {
  changeset.current.forEach((element) => {
    console.log(gridApi.getRowNode(element));
    // 保存処理が終わったら背景色のクリア処理が必要
  });
};

<Button variant="contained" color="primary" onClick={saveClicked}>
  save
</Button>

<AgGridReact
  onCellValueChanged={onCellValueChanged}
/>
```

# その他
## ダウンロード
![download.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036913/86c0814e-a662-07bc-5434-d018e7b52cd8.gif)
最後はデータのダウンロードです。これもよく使う機能だと思うのですが、非常に簡単に実現できます。ここもソースを見たほうが早いと思うので、早速見ていきましょう！

```jsx
// Ag-Grid の onGridReady で gridApi を変数に格納しておく（前回記事参照）
const downloadClicked = () => {
  gridApi.exportDataAsCsv();
};

<Button variant="contained" color="primary" onClick={downloadClicked}>
  download
</Button>;
```

ダウンロード機能自体は Ag-Grid が用意をしてくれているので、トリガーとなるイベントから関数を呼び出すだけです。難しいところは何もないですね。ちなみに community 版では CSV 出力にしか対応していませんが、enterprise 版では Excel 出力もサポートしていて、上記の記述を `gridApi.exportDataAsExcel()` に変更するだけです。←👀（羨望の眼差し）

また、`gridApi.exportDataAsCsv()` に引数を渡して、区切り文字を変更したり、ダブルクォートでの括りを抑制したり、列ヘッダを出力する事もできます。詳しくは [このあたり](https://www.ag-grid.com/react-grid/csv-export/) を見てみてください。


出力に際して、前述したセルレンダーとバリュー・フォーマッタは適用されないようなので、この点だけ注意してください。

# まとめ
全３回に渡って Ag-Grid を紹介してきましたが、いかがでしたでしょうか？ 充実した機能や高いデザイン性に目が行きがちですが、実装の簡単さや、柔軟なカスタマイズ性も兼ね備えているので、非常に実用的で魅力的なコンポーネントだと感じています。

また、[公式サイトのマニュアル](https://www.ag-grid.com/angular-grid/) が非常に充実していて JavaScript、Angular、React、Vue のサンプル付き解説が用意されているので、知名度の低いコンポーネントにありがちな「困ったときに情報が無い😭」というような問題が起こりにくいのも見逃せないポイントです。

欲を言えば enterprise 版を使いたくなるのですが、community 版でも大抵の要件は実現できると思うので、皆さんも是非一度試してみてください！（開発会社の回し者ではありません）

以上で Ag-Grid の紹介を終わります。最後の投稿が遅くなって申し訳有りませんでした🙏

# 参考リンク
* [ag-Grid 公式ドキュメント](https://www.ag-grid.com/documentation/react/)
* [React 公式ドキュメント](https://ja.reactjs.org/docs/getting-started.html)
* [Material-UI 公式ドキュメント](https://material-ui.com/)