📝
自作コンポーネントのススメ
ヘッダ固定のテーブルコンポーネントを作る
経緯
長らくテーブル表示にreact-bootstrap-table-nextを使っていたのですが、ヘッダをsticky
で固定する機能がなく、最近メンテナンスもされていないということで乗り換えを考えていました。
乗り換え先のライブラリをいくつか検討したのですが、
- これまで作ったテーブル設定をそのまま引き継ぎたい
- これまでの機能を維持したい
- ヘッダを上部に固定(sticky)したい
という希望に合致するものがなかなかなく、ちょっと作ってみるかと思い立ちました。
コンポーネントの仕様
- react-bootstrap-table-nextの設定を引き継げる
- ヘッダを固定できる
- ソートできる
- ソートしたときの表示をわかりやすくする
propsの決定
data
ソートなどを行う対象のデータ(配列)
columns
ヘッダの項目などの設定。
- dataField
dataにアクセスするプロパティ。data.idなど。 - text
thに表示する項目名 - sort
ソート可否(boolean) - formatter
セル(td)に表示する内容をカスタマイズする関数
React.FC<{
data: any[];
columns: {
dataField: string;
text: string;
formatter?: (cell: any, row: any) => JSX.Element;
sort?: boolean;
}[];
}
stateの決定
主にソート機能だけなのですが、
- どの列が選択されているか
- asc/descどちらにソートするか
という状態を持つのがよいと思いました。
また、propでもらったdataをソートして返すためにuseMemoを使います。thにonClick
イベントを仕込み、クリックされたら選択状態とasc/descの向きを更新するように実装します。また、ソート不可の場合は処理をスキップします。
style
テーブルのスタイルは元々グローバルCSSで定義しているので、不足分のみ今回のコンポーネントにscopedで定義しています。
caretのアイコンのみ外部のライブラリに依存しています。
最終的なコード
import { faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useCallback, useMemo, useState } from "react";
const StickyTable: React.FC<{
data: any[];
columns: {
dataField: string;
text: string;
classes: string;
formatter?: (cell: any, row: any) => JSX.Element;
sort?: boolean;
}[];
}> = ({ columns, data }) => {
const [sortKey, setSortKey] = useState<string>();
const [sortMode, setSortMode] = useState<"asc" | "desc" | "none">("none");
const processedData: any[] = useMemo(() => {
if (sortKey) {
return data.sort((a, b) => {
switch (sortMode) {
case "asc":
return a[sortKey] - b[sortKey] > 0 ? 1 : -1;
case "desc":
return b[sortKey] - a[sortKey] > 0 ? 1 : -1;
default:
return null;
}
});
}
return data;
}, [data, sortKey, sortMode]);
const isActive = useCallback(
(dataField: string) => {
return dataField === sortKey;
},
[sortKey]
);
return (
<div
style={{
overflow: "scroll",
maxHeight: "70vh",
}}
>
<table>
<thead>
{columns.map((column) => (
<th
className={`${isActive(column.dataField) && "active"}`}
onClick={() => {
if (!column.sort) return;
setSortKey(column.dataField);
setSortMode(sortMode === "asc" ? "desc" : "asc");
}}
>
{column.text}
{column.sort && (
<div className="order">
{!isActive(column.dataField) && (
<>
<div>
<FontAwesomeIcon icon={faCaretDown} color="gray" />
</div>
<div>
<FontAwesomeIcon icon={faCaretUp} color="gray" />
</div>
</>
)}
{isActive(column.dataField) && (
<>
{sortMode === "desc" && (
<div>
<FontAwesomeIcon icon={faCaretDown} />
</div>
)}
{sortMode === "asc" && (
<div>
<FontAwesomeIcon icon={faCaretUp} />
</div>
)}
</>
)}
</div>
)}
</th>
))}
</thead>
<tbody>
{processedData.map((row) => (
<tr>
{columns.map((col) => (
<>
{col.formatter ? (
<td>{col.formatter(row[col.dataField], row)}</td>
) : (
<td>{row[col.dataField]}</td>
)}
</>
))}
</tr>
))}
</tbody>
</table>
<style jsx>
{`
thead th {
background-color: white;
color: #555;
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 1;
}
th.active {
background-color: #4ab9e6 !important;
color: white !important;
}
thead th:first-child {
z-index: 2;
}
`}
</style>
</div>
);
};
export default StickyTable;
Discussion