AgGridのデータのセルのInとOutを整理

こんにちは、キャディでソフトウェアエンジニアをしている小倉です。今はフロントエンドを主に触っています。

いきなりまとめ(TL;DR)

本記事は、AgGridのセルの値のReadとWriteについての機構をまとめた記事になります。 記事が長くなってしまったので、触れる内容をまとめた図を先に持ってきました。 やり方が複数あるものについては以下のように考えておくと良いです。

  • valueSetter,valueGetterは使わなくて良いなら使わないようにして、なるべくfield指定だけですむようにrowDataを設計する。
  • 文字列処理だけで住むときはvalueFormatter, それ以上の処理(CSSで飾り付ける、リッチな機能をつける、etc...)が必要ならcellRenderer

プロローグ

キャディで作っているTechプロダクトの半数は、人力で回していたオペレーションを代替する立ち位置のものです。どうすればうまくいくのかわからない製造業の課題に対してまずは人力で試して、手応えがあったら、効率化や規模拡大のためにTechプロダクト化する、というよくある流れですね。

さて、このようなプロダクト開発に携わっている方が必ずといって良いほどユーザーから聞くフィードバックがあります。 「Excelスプレッドシートの方が便利」 人力で回すオペレーションはUIとDBの部分を表計算ソフトで間に合わせていることが多いです。プロダクト化しても良さそうだぞとなるオペレーションはだいたい強力なExcel・スプシが出来上がっていることでしょう。それと比較されて出てくるフィードバックです。

表計算ソフトはUIとデザインを一緒くたにして使ってしまうと甚大なアンチパターンを踏んでしまいがちですが、そこをちゃんとすれば、洗練された迅速なオペーレーションを回す手助けをしてくれます。とくに表計算UIは大量のデータを扱うのに強かったりします。表計算ソフトのアンチパターンを踏まれないようにFEエンジニアがうまくUIを設計すれば強力なツールとなるでしょう。

そういうわけで、私達のチームでは表計算コンポーネントをプロダクト内に導入することにしました。表計算っぽいコンポーネントを提供するライブラリにはWijmoのFlexGrid, Handsortable, cheetahGridなど様々ありますが、私達はAgGridを使うことにしました。

この記事はなに?

AgGridは機能が多く、同じことを実現するにしても様々な書き方ができてしまいます。とくにセルへの入出力まわりはその傾向が顕著です。 公式ページをよく読めばどういうときにどの機能を使うのかのポリシーが書いてあるのですが、開発のたびに参照しに行くのも面倒なので、一覧性を重視したまとめドキュメントを作ることにしました。この記事はそのドキュメントをブログ向けにアレンジしたものです。

用語

Grid:表全体 Row:行 Cell:マス一つ

行は「どのオブジェクトか」、列は「オブジェクトをどう料理するか」

AgGirdでは各行(Row)は1つのオブジェクトと対応します。つまりGrid全体としてはオブジェクトの配列になります。その配列がしばしばrowDataと呼ばれるものです。 各列(Column)は、対応するオブジェクトをどう料理するかに対応します。ここでいう「料理」は大抵の場合は「オブジェクトのこのフィールドの値をtoStringした値を表示」になります。それ以外にも、複数フィールドの合計値を計算して黄色い背景色で表示、などの複雑なパターンにも対応可能です。 つまり、$i$行目$j$列目の場所にあるCellの値はrowData[i]をj列目共通の料理の仕方で得られた値になります。(UI上で列や行の並びを変えられるので厳密な説明ではありませんが、、、)

コンポーネントの例

基本的にはAgGridReactコンポーネントに行データのArray(rowData)をPropsで与えると、Gridが出来上がります。 AgGridReactのchildrenとしてAgGridColumnを追加すると列に関する情報を入れることができます。 AgGridColumnfield属性にフィールド名を入れておくとi行目の当該列はrowData[i]のそのフィールドの値をtoStringした値が表示されます。

▼素朴なAgGridコンポーネント

import { AgGridColumn, AgGridReact } from "ag-grid-react";
import "ag-grid-community/dist/styles/ag-grid.css";
import "ag-grid-community/dist/styles/ag-theme-alpine.css";

const rowData = [
  { firstName: "Taro", lastName: "Tanaka", age: 10 },
  { firstName: "Hanako", lastName: "Yamada", age: 25 },
  { firstName: "Jiro", lastName: "Suzuki", age: 32 },
];

export const MyGrid: React.VFC = () => {
  return (
    <div className="ag-theme-alpine" >
      <AgGridReact rowData={rowData}>
        <AgGridColumn field="firstName" editable={true} />
        <AgGridColumn field="lastName" editable={true} />
        <AgGridColumn field="age" />
      </AgGridReact>
    </div>
  );
};

▼見え方

表示

上で説明した「料理」は、おおまかに以下の2パートに分かれます。 - オブジェクト(rowData)から値を計算する部分 - 値を表示する部分

オブジェクトから値を計算する部分

オブジェクトから値を計算する部分は以下の2種類のパターンがあります。

①フィールドを指定する

<AgGridColumn field="lastName" />のようにGridCloumnのfield属性を指定すると、その列のセルの値は、オブジェクトのそのフィールドの値になります。次に紹介するvalueGetterが特に指定されていないときは、フィールド指定だと思って値を取ってこようとします。たとえfield属性が指定されていなかったり、存在しないフィールド名が指定されていたとしても、そのフィールドにアクセスして値を取得しようとするので、undefinedがそのセルの値になります。AgGridは全体的にフィールド指定で済むならばその方がコードがスッキリするようになっていますから、なるべくフィールド指定で済むようにrowDataの設計をするとよいでしょう。

②valueGetterを指定する

<AgGridColumn valueGetter={myValueGetter} />のようにGridCloumnのvalueGetter属性に関数を指定すると、その列のセルの値は、その関数が返す値になります。ここでvalueGetter関数には、引数としてその行のオブジェクト(rowDataの要素)が渡されるだけでなく、Grid全体を操作できるAPIなどいろいろおまけがついてきます。そのため実質Grid全体の情報を使ってなんでも計算できることにります。詳細な引数は公式ドキュメントを参照してください。どうしても複数フィールドにアクセスしないといけない場合や、複数のセルで同じ値を共通して参照しなければならない場合にvalueGetterを使うとよいでしょう。

値を表示する部分

値を表示する部分は以下の3種類のパターンがあります。

①toStringして得られる文字列を表示する

とくに何も指定がない場合は、その列のセルは、セルの値をObject.toStringして得られる文字列が表示されます。値そのものがテキストや数値で、そのまま表示していいときは、このパターンで実装するとよいでしょう。

②valueFormatter関数によってフォーマットした文字列を表示する

<AgGridColumn valueFormatter={myValueFormatter} />のようにGridCloumnのvalueFormatterk属性に関数を指定すると、その列のセルは、その関数が返す文字列を表示するようになります。なお、次に説明するcellRendererが指定されている場合はそちらが優先されます。 ValueFormatterr関数に渡される引数の詳細は公式ドキュメントを参照してください。値そのものがテキストや数値で、フォーマットし文字列を表示するだけでよい場合にこのパターンで実装するとよいでしょう。

③cellRendererを指定してJSX/TSXを描画する

<AgGridColumn cellRenderer={myCellRenderer} />のようにGridCloumnのcellRenderer属性に関数などを指定すると、その列のセルは、それによって計算されるJSXやHTMLを表示するようになります。cellRendererはJSX/HTMLを返す関数、その関数名(string)、レンダラーのコンポーネント、を指定することができます。もしReactのFunctionalComponentを渡したい場合はcellRenderer属性ではなくcellRendererFramework属性にFunctionalComponentを渡してあげなければならないことに注意してください。 cellRenderer関数に渡される引数の詳細は公式ドキュメントを参照してください。値そのものがテキストや数値で、フォーマットし文字列を表示するだけでよい場合にこのパターンで実装するとよいでしょう。

編集

<AgGridColumn editable={true} />のようにGridColumnのeditable属性をtrueにするとその列のセルは編集可能になります。 編集は以下の3パートに分かれます - Editorが編集後の値を返す部分 - 編集後の値をパースしてセルの値を更新する部分 - 更新されたセルの値に応じて、オブジェクト(rowData)の値を更新する部分

Editorが編集後の値を返す部分

<AgGridColumn cellEditor="agSelectCellEditor">のようにGridColumnのcellEditor属性を指定すると、その列のセルを指定されたEditorコンポーネントを通じて編集できるようになります。このEditorはgetValueというメンバ関数を持っている必要があります。これは編集終了時に呼び出される関数であり、編集後の値を返すことが期待されます。 AgGridは公式に何種類かのcellEditorを用意していますが、自前でEditorを実装することも可能です。そのときはgetValueメソッドを実装してあげることを忘れないように、、、 もしReactのFunctionalComponentを渡したい場合はcellEditor属性ではなくcellEditorFramework属性にFunctionalComponentを渡してあげなければならないことに注意してください。また筆者は今回AgGridを使って初めて知ったのですが、FunctionalComponentにメンバ関数を追加したい場合はuseImerativeHandleフックを使うとよいです。

編集後の値をパースしてセルの値を更新する部分

EditorのgetValueメソッドを通じて得られた値は通常そのままセルの値としてみなされます。しかし、何かしらパースしたい場合は<AgGridColumn valueParser={myValueParser}>のようにGridColumnのvalueParser属性にパース関数を指定することで、パース処理をはさむことができます。valueParser関数に渡される引数の詳細は公式ドキュメントを参照してください。

更新されたセルの値に応じて、オブジェクト(rowData)の値を更新する部分

更新されたセルの値に応じて、オブジェクト(rowData)の値を更新する部分には2種類のパターンがあります。

①フィールドを指定する

<AgGridColumn field="lastName" />のようにGridCloumnのfield属性を指定すると、その列のセルの値は、オブジェクトのそのフィールドの値を上書きします。次に紹介するvaluSetterが指定されていないときは、フィールド指定だと思って値を書き込もうとします。

②valueGetterを指定する

<AgGridColumn valueSetter={myValueSetter} />のようにGridCloumnのvalueSetter属性に関数を指定すると、その列のセルの値(編集後の値)が引数として関数に渡されセルの値が更新する処理が走ります。valueSetterの詳細な引数は公式ドキュメントを参照してください。

有償プランの機能

AgGridの有償版にはたくさんの便利機能がついています。この記事ではそのうちコピーペーストと補完について触れます。ここで扱う機能は有償ライセンスがなくても試すことは可能です(ただし、ウォーターマークが入る)。 どの機能もファイルの先頭にimport "ag-grid-enterprise";を追加しないと有効にならないので注意です。

コピー

コピー時の挙動はカラムではなくAgGrid全体に共通のコールバック関数を指定する必要があります。 <AgGridReact processCellForClipboard={myCallBack}/>のようにGridのprocessCellForClipboard属性に関数を指定すると、選択したセルがコピーされたときに、その関数の返り値(string)がクリップボードにコピーされるようになります。複数セル選択でコピーしたときは各セルごとにこの関数が計算した文字列がTSV形式でクリップボードにコピーされます(\t 以外のデリミタを指定することも可能)。 この関数はどの列も共通なので、どの列から呼び出されたかは関数内で判定しないといけません。以下のようにcolIdで判定するとよいでしょう。

const processCellForClipboard = (params: ProcessCellForExportParams) => {
  const {column} = params;
  const colId = column.getColId();
  if(colId === 'lastName')return "LastName" + params.value;
  return params.value;
};

ペースト

ペースト時の挙動もカラムではなくAgGrid全体に共通のコールバック関数を指定する必要があります。 <AgGridReact processCellFromClipboard={myCallBack}/>のようにGridのprocessCellFromClipboard属性に関数を指定すると、選択したセルにペーストされたときに、クリップボードの文字列がその関数に渡され、返り値をセルの新しい値として更新します。複数セル選択でペーストしたときはクリップボードの文字列をTSVだと思って、よしなに文字列を分割してくれます。 この関数はどの列も共通なので、どの列から呼び出されたかは関数内で判定しないといけません。以下のようにcolIdで判定するとよいでしょう。

const processCellFromClipboard = (params: ProcessCellForExportParams) => {
  const {column} = params;
  const colId = column.getColId();
  if(colId === 'lastName')return "LastName" + params.value;
  return params.value;
};

補完

補完はセルの選択範囲の右下の小さな四角をドラッグしながら選択範囲を広げることで、よしなに値を産めてくれる機能です。言葉で説明するより以下のGIFを見たほうが早いかもしれません。

この機能は<AgGridReact enableRangeSelection={true} enableFillHandle={true}/>のようにenableFillHandleをtrueにすると有効になります。 デフォルトだと、数値の場合は多項式補完、そうでない場合は周期的に補完してくれるようです。もしそれ以外の方法で補完したい場合は <AgGridReact fillOperation={myFillOperation}/>のようにGridのfillOperation属性に関数を指定するとよいでしょう。 この関数はどの列も共通なので、どの列から呼び出されたかは関数内で判定しないといけません。以下のようにcolIdで判定するとよいでしょう。

const fillOperation = (params: FillOperationParams) => {
  const { column, initialValues } = params;
  const colId = column.getColId();
  if (colId === "age") return initialValues.reduce((accum, val) => accum + val, 0);
  return "";
};

まとめ(再掲)

本記事で触れた内容をまとめた図です。 やり方が複数あるものについては以下のように考えておくと良いです。 - valueSetter,valueGetterは使わなくて良いなら使わないようにして、なるべくfield指定だけですむようにrowDataを設計する。 - 文字列処理だけで住むときはvalueFormatter, それ以上の処理(CSSで飾り付ける、リッチな機能をつける、etc...)が必要ならcellRenderer

最後に

AGGridにはほかにもたくさんの機能があります。表計算っぽいUIが欲しくなったらぜひ一度使用を検討してい見てください!