TanStack Table 8とバニラJSを使ってみよう

著者
Damian
Terlecki
7分間の読書
JS

最近、TanStack TableというミニマルでヘッドレスなJSテーブルライブラリに出会いました。 特に、他の多機能で自己主張の強いソリューションを扱った後だったので、とても新鮮に感じました。 TanStack Table 8は、ReactJS、Svelte、Vueなどの最も人気のあるWeb UI構築ライブラリと、 別のモジュールを通じてうまく統合できます。そしてドキュメントによれば、バニラJSを使っても 他のライブラリと同様に利用できるとのことです。

TanStack Tableデモ

現在、他のアプローチとは異なり、バニラJSでの使用例は提供されていません。 良い出発点は、選択したフレームワーク内のサンプルプロジェクトと、統合モジュールの簡単な実装を調べることです。 それを踏まえて、皆さんのためにTanStack TableのバニラJSデモを用意しました。

TanStack Table 8とバニラJS

まず最初に、一般的な入門ガイドをご覧ください。 そこにはテーブルモデルを構築するために必要なステップが説明されています。 始めるには、コアライブラリ@tanstack/table-core@8.11.6を 依存関係に追加します。簡単にするために、ここでは unpkg.comサービスを使ってページに読み込みます。これにより、 環境設定の手間が省けます。

<head>
  <title>Vanilla JS Demo TanStack 8.11.6 Core UMD Table</title>
  <script src="https://unpkg.com/@tanstack/table-core@8.11.6/build/umd/index.development.js"></script>
</head>

次のステップは、テーブルを配置する場所をHTMLのbodyに用意することです。

<body>
    <h1>Demo TanStack 8.11.6 Core UMD Table</h1>
    <div id="table-root"></div>
</body>

次に、ダミーデータを準備しましょう。 キーと値のプロパティからなるオブジェクトのJSON配列でかまいません。

<script>
const data = [
    {
        fullName: "Alice Johnson",
        position: "Software Engineer",
        department: "Engineering",
        yearsOfService: 3
    },
    {
        fullName: "Bob Smith",
        position: "Marketing Specialist",
        department: "Marketing",
        yearsOfService: 7
    },//...
];//...
</script>

テーブルモデルの作成に移ります。 データに加えて、カラム定義を作成する必要があります。 これらは特定の構造を持つべきで、createColumnHelper()accessor()ユーティリティを使って それに従うことができます。 インポートされたUMDモジュールを振り返ると、それはTableCoreというグローバル変数名でエクスポートされています。

const columnHelper = TableCore.createColumnHelper();
const columns = [
    columnHelper.accessor(row => row.fullName, {
        id: 'fullName',
        cell: info => info.getValue(),
        footer: info => info.column.id,
    }),
    columnHelper.accessor(row => row.position, {
        id: 'position',
        cell: info => `<i>${info.getValue()}</i>`,
        header: () => `<span>Position</span>`,
        footer: info => info.column.id,
    }),//...
];

これらのアクセサは、テーブルコンテンツの周りに基本的なテンプレートを提供するクリーンな方法を提供します。 後でパラメータをいじって、どのように機能するかを確認してみてください。 最後に、テーブルモデルを構築できます。いくつかのプロパティが必須と見なされていたため、 何度か試行錯誤が必要でした。

const table = TableCore.createTable({
    data,
    columns,
    getCoreRowModel: TableCore.getCoreRowModel(),
    state: {
        columnPinning: {},
        pagination: {},
    },
    debugAll: true,
});

TanStack Tableはヘッドレス実装なので、 自分でDOMを構築する必要があります。バニラJSでのDOM作成は少々面倒ですが、それほど難しいわけではありません。 テーブルモデルから、ヘッダー、行、フッターの情報を取得できます。 これらをcreateElementelement.innerHTML DOM APIから作成した テーブル要素にマッピングできます。

drawTable("table-root", table);

function drawTable(rootElementId, tableModel) {
    const rootElement = document.getElementById(rootElementId);
    const tableElement = document.createElement("table");
    const thead = document.createElement("thead");
    const tbody = document.createElement("tbody");
    const tfoot = document.createElement("tfoot");

    thead.append(...tableModel.getHeaderGroups().map(headerGroup => {
        const rowElement = document.createElement("tr");
        rowElement.append(...headerGroup.headers.map(header => {
            const cellElement = document.createElement("th");
            cellElement.innerHTML = flexRender(header.column.columnDef.header, header.getContext());
            return cellElement;
        }));
        return rowElement;
    }));
    // 

    tbody.append(...tableModel.getRowModel().rows.map(row => {
        const rowElement = document.createElement("tr");
        rowElement.append(...row.getVisibleCells().map(cell => {
            const cellElement = document.createElement("td");
            cellElement.innerHTML = flexRender(cell.column.columnDef.cell, cell.getContext());
            return cellElement;
        }));
        return rowElement;
    }));

    tfoot.append(...tableModel.getFooterGroups().map(footerGroup => {
        const rowElement = document.createElement("tr");
        rowElement.append(...footerGroup.headers.map(header => {
            const cellElement = document.createElement("th");
            cellElement.innerHTML = flexRender(header.column.columnDef.footer, header.getContext());
            return cellElement;
        }));
        return rowElement;
    }));
    tableElement.append(thead, tbody, tfoot);
    tableElement.id = rootElementId;
    rootElement.replaceWith(tableElement);

    function flexRender(renderer, context) {
        // if the content is unsafe, you can sanitize it here
        if (typeof renderer === "function") {
            return renderer(context);
        }
        return renderer
    }
}

最後に、ルート要素をテーブル要素で置き換えます。 ヘッドレスな性質のおかげで、テーブル要素をまったく使用する必要はありません。カスタムのテーブル構造を作成できます。 flexRenderを通じて、他のUIフレームワークとテンプレートを統合することもできます。

おまけのショーケース – ソート

単純なモデルの作成方法と、それがDOMとどう関係するかがわかったので、ドキュメントでより高度なオプションを調べることができます。 もう1つ、非常に重要な学習ステップがあります。モデルの状態が変わるたびに、テーブルを再描画する必要があります。 これをどう行うかを見つけ出すには時間がかかるかもしれないので、ソートのショーケースを通じてその方法をお見せします。

ソート機能を追加するには、モデル設定に追加のオプションを含める必要があります。


const table = TableCore.createTable({
    //...
    getSortedRowModel: TableCore.getSortedRowModel(),
    //...
});

ソートは、列ヘッダーのgetToggleSortingHandler()関数から返されるハンドラを呼び出すことで有効化および切り替えができます。 drawTable関数内で、ヘッダーセル要素のonclick関数にバインドできます。

thead.append(...tableModel.getHeaderGroups().map(headerGroup => {
    const rowElement = document.createElement("tr");
    rowElement.append(...headerGroup.headers.map(header => {
        const cellElement = document.createElement("th");
        cellElement.innerHTML = flexRender(header.column.columnDef.header, header.getContext());
        cellElement.onclick = header.column.getToggleSortingHandler()
        if (header.column.getIsSorted()) {
            cellElement.innerHTML += header.column.getIsSorted() === 'asc' ? '↑' : '↓'
        }
        return cellElement;
    }));
    return rowElement;
}));

最後に、ソートの変更時にテーブルを再描画する必要があります。さもないと、視覚的に何も変わりません。 最善の方法は、onStateChangeのようなテーブルの状態変更に関するグローバルリスナーを介することです。 しかし、このテーブルオプションは設定が厄介です。なぜなら、そうするとコアの状態セッターがアンバインドされてしまうからです。 幸いなことに、これは意図されたものであり、ドキュメントを調べた後、再帰をトリガーしないtable.setOptionsでバインドし直します。

const table = TableCore.createTable({
    //...
    onStateChange: (foo) => {
        table.setOptions(prev => ({
            ...prev,
            state: foo(table.getState())
        }));
        drawTable("table-root", table)
    }
});

最終的なテーブルデモは次のようになり、動作するはずです。