TanStack Tableで動的にヘッダーグループを生成する 2024.06.01
TanStackはreact-queryでおなじみのオープンソースライブラリスタック。TanStack Tableはヘッドレスのデータグリッドライブラリ。shadcn/uiのData TableはTanStack Tableをロジック部分に使用している。
TanStack TableはHTMLのcolspan
のように複数の列にまたがったテーブルヘッダーを表示することができるが、これを動的に生成したい。
基本のテーブル
いちおう、ヘッダーグループがない基本の実装方法を見ておく。テーブルを表示する詳細は省略する。
// .----.----.----.----.
// | K1 | K2 | K3 | K4 | Header (key)
// |----|----|----|----|
// | V1 | V2 | V3 | V4 | Data (value)
// |----|----|----|----|
// | : | : | : | : |
const data = [ // 行
{
[K1]: { ...V1 }, // 列
[K2]: { ...V2 },
[K3]: { ...V3 },
[K4]: { ...V4 },
},
// ...
];
const columns = [ // 列
columnHelper.accessor(K1, { /* ここでヘッダーやセルの表示設定ができる */ }),
columnHelper.accessor(K2, {
header: () => <span>Header</span>,
cell: (info) => info.getValue(),
}),
columnHelper.accessor(K3, { /**/ }),
columnHelper.accessor(K4, { /**/ }),
];
const table = useReactTable({ data, columns });
動的に生成するには以下のようにすればよさそう。
// .----.----.----.----.---
// | K1 | K2 | K3 | K4 | .. Header
// |----|----|----|----|---
// | V1 | V2 | V3 | V4 | .. Data
// |----|----|----|----|---
// | : | : | : | : |
const data = [ // 行
{
[K1]: { ...V1 }, // 列
[K2]: { ...V2 },
[K3]: { ...V3 },
[K4]: { ...V4 },
// ... ここを動的にしたい
},
// ...
];
// 1行目のデータからカラムを生成
const columns = Object.entries(data[0]).map(([key, value]) => {
return columnHelper.accessor(key, { /**/ });
});
const table = useReactTable({ data, columns });
固定のヘッダーグループ
サンプルにあるような固定の結合ヘッダーはcolumns
の構造をネストするだけで表示できる。columns
にヘッダーグループと列をcolumnHelper
の階層構造でわたし、data
に行列データをわたす。
// .---------.---------.
// | G1 | G2 | Header Group
// |----.----|----.----|
// | K1 | K2 | K3 | K4 | Header
// |----|----|----|----|
// | V1 | V2 | V3 | V4 | Data
// |----|----|----|----|
// | : | : | : | : |
const columns = [ // ヘッダーグループ
columnHelper.group({
id: G1,
name: "Group 1",
columns: [ // 列
columnHelper.accessor(K1, { /**/ }),
columnHelper.accessor(K2, { /**/ }),
],
}),
columnHelper.group({
id: G2,
name: "Group 2",
columns: [
columnHelper.accessor(K3, { /**/ }),
columnHelper.accessor(K4, { /**/ }),
],
}),
];
const data = [ // 行
{
[K1]: { ...V1 }, // 列
[K2]: { ...V2 },
[K3]: { ...V3 },
[K4]: { ...V4 },
},
// ...
];
const table = useReactTable({ data, columns });
動的なヘッダーとヘッダーグループ
これまでに得た情報から、動的にヘッダーグループを生成するには以下の2つの構造を元データを変形して生成する必要がありそうだ。
- ヘッダーグループと列をネストしてイテレーションするための構造
- ヘッダーグループを除いた、データとしてわたす構造
columns
とdata
とで異なる構造を取るので、まだ直感的にこう書けばいいという確信が得られていない。
まずは素直にカラム生成用の3階層のデータを構成することにして、データはヘッダーグループ階層を取り除いて取得することにする。
このデータは、行→ヘッダーグループ→列の3つの階層で構成されており、ヘッダーグループを取り除くと行→列の階層だけが得られる。
// .---------.---------.---
// | G1 | G2 | .. Header Group
// |----.----|----.----|---
// | K1 | K2 | K3 | K4 | .. Header
// |----|----|----|----|---
// | V1 | V2 | V3 | V4 | .. Data
// '----'----'----'----'---
// | : | : | : | : |
const original = [ // 行
[ // ヘッダーグループ
{
id: G1,
name: "Group 1",
children: [ // 列
{ id: K1, ...V1 },
{ id: K2, ...V2 },
// ...
],
},
{
id: G2,
name: "Group 2",
children: [ // 列
{ id: K3, ...V3 },
{ id: K4, ...V4 },
// ...
],
}
// ...
],
// ...
];
// 1行目をカラムの生成に利用する
const columns = original[0].map((headerGroup) => { // ヘッダーグループ
return columnHelper.group({
id: headerGroup.id,
header: () => headerGroup.name,
columns: headerGroup.children.map((header) => { // 列
return columnHelper.accessor(header.id, { /**/ });
}),
});
});
// 行→列の階層だけ取り出す。列はKey-Valueになるように変形する
// ```
// const data = [ // 行
// { // 列
// K1: { id: K1, ...V1 }
// K2: { id: K2, ...V2 }
// }
// // ...
// ]
// ```
const data = original.map((row) => { // 行
// 列をオブジェクト形式にする
return Object.fromEntries(
// flatMapを使用してヘッダーグループを捨てる
row
.flatMap((headerGroup) => headerGroup.children)
// オブジェクトにするためタプルで返す
.map((header) => [header.id, header]),
);
});
const table = useReactTable({ data, columns });
これで動的なヘッダーグループを持つテーブルを表示することができる。元のデータ構造にもよるが、あまり効率的ではないように思えるので、もっといい方法がないか検討してみる。
たとえば逆に、行列データからカラム階層を生成することも考えてみる。各列に所属するヘッダーグループを指定する列を追加し、ヘッダーグループの階層はgroupBy
のような処理で生成する。コードは直感的にわかりにくいが、構造はシンプルになった。
なんとなく、こっちのほうが本筋のように思える。
// 元データはそのまま`data`にわたせる構造
const data = [ // 行
{ // 列
[K1]: {
id: K1,
groupId: G1, // ヘッダーグループ
groupName: 'Group 1',
...V1, // その他のデータ
},
[K2]: {
id: K2,
groupId: G1,
groupName: 'Group 1',
...V2, // その他のデータ
},
// ...
},
// ...
]
// ヘッダーグループとヘッダーの構造を生成する
// groupIdでカラムをグループ化し、Object.entriesやObject.valuesで配列に戻す
const columns = Object.entries(
Object.groupBy(Object.values(data[0]), ({ groupId } => groupId))
).map(([groupId, headers]) => { // ヘッダーグループ
return columnHelper.group({
id: groupId,
header: () => headers[0].groupName, // 各列がグループの情報も持っている
columns: headers.map((header) => { // 列
return columnHelper.accessor(header.id, {/**/})
})
})
})
const table = useReactTable({ data, columns })
※Object.groupByを使用するには最新のブラウザや、v22以上のNode.js環境が必要。環境に応じてLodashのgroupBy
などのpolyfillを使用する。