SvelteKit SkeletonでDataTableを使いやすくしてみた ~データを渡すだけでテーブル表示~

# はじめに
:baby: 「SvelteにハマってUIライブラリは何がいいか検討したところ、Skeletonがいい感じでした。今回はお手本を参考に作成したDataTableをいくつか改善していこうと思います。」


https://www.skeleton.dev/docs/ssd
# 前提
<ul>
    <li>Node.js v22.6.0(Docker)</li>
    <li>SvelteKit - アプリケーション作成済み</li>
    <li>Skeleton - お手本を参考にDataTableを作成済み</li>
    <li>MySql - テストデータを登録済み</li>
</ul>

DataTableの作成は以下の記事で行いました。

https://qiita.com/miyazaki-dev/items/ecb2ddb5546e65fafb2a

:::note
※これを読んでない人でも十分理解できる内容となっております。
:::

![画面収録 2024-08-14 23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3484672/2e62f3f9-6122-5cc5-b500-03eeaa684396.gif)
 
 # 改善したい点
 <ol>
     <li>データを渡すだけでテーブル表示するコンポーネントにしたい :point_left_tone3:今ここ</li>
     <li>Select可能なテーブルにできるようにしたい</li>
     <li>Formの要素として使いやすくしたい</li>
</ol>

## 1.データを渡すだけでテーブル表示するコンポーネントにしたい
現在、DataTableコンポーネントは以下のようになっています。
/lib/components/Datatable.svelte
```svelte
<script lang="ts">
    import { DataHandler } from "@vincjo/datatables";
    import type { UserInterface } from "../../routes/UserList/userInterface";
    import Search from "./Search.svelte";
    import ThSort from "./ThSort.svelte";
    import ThFilter from "./ThFilter.svelte";
    import Pagination from "./Pagination.svelte";
    import RowCount from "./RowCount.svelte";
    import RowsPerPage from "./RowsPerPage.svelte";
    export let data: UserInterface[];
    const handler = new DataHandler(data, {rowsPerPage: 5});
    const rows = handler.getRows();
</script>


<div class="table-container space-y-4">
    <header class="flex justify-between gap-4">
        <Search {handler} searchFields={["username","deptC"]} />
        <RowsPerPage {handler} />
    </header>
    <table class="table table-hover table-compact table-auto w-full">
        <thead>
            <tr>
                <ThSort {handler} orderBy="username">名前</ThSort>
                <ThSort {handler} orderBy="email">Email</ThSort>
                <ThSort {handler} orderBy="deptC">部署C</ThSort>
            </tr>
            <tr>
                <ThFilter {handler} filterBy="username" />
                <ThFilter {handler} filterBy="email" />
                <ThFilter {handler} filterBy="deptC" />
            </tr>
        </thead>
        <tbody>
            {#each $rows as row}
                <tr>
                    <td>{row.username}</td>
                    <td>{row.email}</td>
                    <td>{row.deptC}</td>
                </tr>
            {/each}
        </tbody>
    </table>
    <footer class="flex justify-between">
        <RowCount {handler} />
        <Pagination {handler} />
    </footer>
</div>
```
:robot:「`export let data:UserInterface[]`としていて、
　　インポート先で取得したデータを使用できるようにしています。
　　`<ThSort {handler} orderBy="username">名前</ThSort>`のように
  　　列名は固定となっています。」

:baby:「でも、どんな型のデータでも渡せるようにして、列名も固定じゃないほうがいいよね」

:robot:「それではまず、scriptタグ部分を以下のように修正してみました」
### scriptタグ部分の修正
Datatable.svelte
```diff_svelte
+ <script context="module" lang="ts">
+     export type RowData = Record<string, any>;　//行データは任意のキーとそれに対応する型(any)を持つとする
+     export type ColumnData = {  //列データは列名と任意のキーを持つとする
+          key: string;
+          name: string; 
+     };
+ </script>
  <script lang="ts">
    import { DataHandler } from "@vincjo/datatables";
-   import type { UserInterface } from "../../routes/UserList/userInterface";
    import Search from "./Search.svelte";
    import ThSort from "./ThSort.svelte";
    import ThFilter from "./ThFilter.svelte";
    import Pagination from "./Pagination.svelte";
    import RowCount from "./RowCount.svelte";
    import RowsPerPage from "./RowsPerPage.svelte";
    
-   export let data: UserInterface[]; //テーブルに表示するデータはUserInterface[]型しか受け取らないのを廃止
+   export let data: RowData[];
+   export let columns: ColumnData[];
   
    const handler = new DataHandler(data, {rowsPerPage: 5});
    const rows = handler.getRows();
```
:robot:「Datatableコンポーネントが受け取るdataを、UserInterface型以外でも
受け取れるようにしました。また、列情報も受け取るようにしました。
これで列情報のkeyを使って行データの値を取得する予定です。」

:baby:「`<script context="module" lang="ts">`って何？」
:robot: 「ここに含まれるコードは、コンポーネントがインスタンス化されたときではなく、モジュールが最初に評価されたときに一度だけ実行されグローバルにアクセス可能です。」
「`script lang="ts">内で定義された型はコンポーネントのインスタンスに閉じているため、他の部分から参照することができません。」

https://learn.svelte.jp/tutorial/sharing-code

### html部分の修正
Datatable.svelte
```diff_svelte
<div class="table-container space-y-4">
    <header class="flex justify-between gap-4">
-        <Search {handler} searchFields={["username","deptC"]} />
+        <Search {handler} searchFields={[columns[0].key]} /> ←Searchフィールドはとりあえず、1列目のデータを検索するように指定。
        <RowsPerPage {handler} />
    </header>
    <table class="table table-hover table-compact table-auto w-full">
        <thead>
            <tr>
-               <ThSort {handler} orderBy="username">名前</ThSort>
-               <ThSort {handler} orderBy="email">Email</ThSort>
-               <ThSort {handler} orderBy="deptC">部署C</ThSort>
+               {#each columns as column}
+                   <ThSort {handler} orderBy={column.key}>{column.name}</ThSort>
+               {/each}
            </tr>
            <tr>
-               <ThFilter {handler} filterBy="username" />
-               <ThFilter {handler} filterBy="email" />
-               <ThFilter {handler} filterBy="deptC" />
+               {#each columns as column}
+                   <ThFilter {handler} filterBy={column.key} />
+               {/each}
            </tr>
        </thead>
        <tbody>
            {#each $rows as row}
                <tr>
-                   <td>{row.username}</td>
-                   <td>{row.email}</td>
-                   <td>{row.deptC}</td>
+                   {#each columns as column}
+                       <td>{row[column.key]}</td> ←与えたdataで列データと一致するデータを表示
+                   {/each}
                </tr>
            {/each}
        </tbody>
    </table>
    <footer class="flex justify-between">
        <RowCount {handler} />
        <Pagination {handler} />
    </footer>
</div>
```
:baby:「列データを渡すようにすれば、dataの中でテーブルに表示したい値を指定できるね」
### インポート先の修正
+page.svelte
```diff_svelte
<script lang="ts">
    import type { UserInterface } from './userInterface.js';
-    import Datatable from '$lib/components/Datatable.svelte';
+    import Datatable, { type ColumnData } from '$lib/components/Datatable.svelte';
    export let data: {userList: UserInterface[]};
    let userList: UserInterface[] = [];
    userList = data.userList;
+   let columns:ColumnData[] = [
+      {name:"名前", key:"username"},
+      {name:"Email", key:"email"},
+      {name:"部署C", key:"deptC"}
+   ];
</script>
<h1>ユーザー一覧</h1>
<div style="width: 70%;">
    <Datatable
        data={userList}
+       columns={columns}
    />
</div>
```
:robot:「ColumnDataをインポート先で作成して、Datatableコンポーネントに渡しています。」

:baby:「Searchコンポーネントで表示している列を検索するなら、Filterコンポーネントでよくない？」

:robot:「......そうですね。気が向いたら直します。」
## Searchコンポーネント修正（2024/8/20)
:robot:「Searchコンポーネントは、好きなだけ追加表示できるようにしたいと思います。」
　　「placeholderも指定できるようにしました。」
Search.svelte
```diff_svelte
<script lang="ts">
	import type { DataHandler, Field } from '@vincjo/datatables';
	export let handler: DataHandler;
    export let searchFields: Field<any>[];
+   export let placeholder = "Search...";
	let value: string;
</script>

<input
	class="input sm:w-64 w-36"
	type="search"
-	placeholder="Search..."
+   placeholder={placeholder}
	bind:value
	on:input={() => handler.search(value, searchFields)}
/>
```
Datatable.svelte
```diff_svelte
<script context="module" lang="ts">
    export type RowData = Record<string, any>;
    export type ColumnData = { name: string; key: string };
+   export type SearchField = { keys: string[]; placeholder: string;}　←keysは一つのSearchComponentが検索する対象の項目
</script>

<script lang="ts">
    import { DataHandler } from "@vincjo/datatables";
    import Search from "./Search.svelte";
    import ThSort from "./ThSort.svelte";
    import ThFilter from "./ThFilter.svelte";
    import Pagination from "./Pagination.svelte";
    import RowCount from "./RowCount.svelte";
    import RowsPerPage from "./RowsPerPage.svelte";

    export let data: RowData[];
    export let columns: ColumnData[];
+   export let searchFields: SearchField[] = [];

    const handler = new DataHandler(data, {rowsPerPage: 5});
    const rows = handler.getRows();
</script>

<div class="table-container space-y-4">
    <header class="flex justify-between gap-4">
-       <Search {handler} searchFields={[columns[0].key]} />
+       {#if searchFields.length > 0} ←searchFieldsを指定しなかった場合は、Searchコンポーネントを表示しません。
+           {#each searchFields as data}
+               <Search {handler} searchFields={data.keys} placeholder={data.placeholder} />
+           {/each}
+       {/if}       
        <RowsPerPage {handler} />
    </header>
    <table class="table table-hover table-compact table-auto w-full">
        <thead>
            <tr>
                {#each columns as column}
                    <ThSort {handler} orderBy={column.key}>{column.name}</ThSort>
                {/each}
            </tr>
            <tr>
                {#each columns as column}
                    <ThFilter {handler} filterBy={column.key} />
                {/each}
            </tr>
        </thead>
        <tbody>
            {#each $rows as row}
                <tr>
                    {#each columns as column}
                        <td>{row[column.key]}</td>
                    {/each}
                </tr>
            {/each}
        </tbody>
    </table>
    <footer class="flex justify-between">
        <RowCount {handler} />
        <Pagination {handler} />
    </footer>
</div>
```
:baby:「これで指定したsearchFieldsの数だけ検索フィールドが表示されるね！」

:robot:「それでは、+page.svelteを修正して、実際に使ってみましょう」
+page.svelte
```diff_svelte
<script lang="ts">
    import type { UserInterface } from './userInterface.js';
-   import Datatable, { type ColumnData } from '$lib/components/Datatable.svelte';
+   import Datatable, { type ColumnData, type SearchField } from '$lib/components/Datatable.svelte';
    
    export let data: {userList: UserInterface[]};
    
    let userList: UserInterface[] = [];
    userList = data.userList;
    
    let columns:ColumnData[] = [
       {name:"名前", key:"username"},
       {name:"Email", key:"email"},
       {name:"部署C", key:"deptC"}
    ];

+   let searchFields: SearchField[] = [
+       {keys:["username","email"], placeholder:"名前またはEmailで検索"},
+       {keys:["user_id"], placeholder:"user_idで検索"}
+   ]
</script>
<h1>ユーザー一覧</h1>
<div style="width: 70%;">
    <Datatable
        data={userList}
        columns={columns}
+       searchFields={searchFields}
    />
</div>
```
:robot:「検索フィールドを二つ表示するように指定してみました。」
　　「一つ目は、"名前"or"Email"で検索し、二つ目は、"user_id"で検索します。」

:baby:「"user_id"はテーブルには表示していない列だけど、dataには存在する項目だったから、これで検索することができるようになったね！」

:robot: 「以下が実行結果になります。」

![画面収録 2024-08-20 23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3484672/715bf949-f87e-3059-cdac-0db7acc8d507.gif)

# 終わりに
まだまだ、改善の余地はありますが、少しは使いやすくなったと思います。
 <ol>
     <li>データを渡すだけでテーブル表示するコンポーネントにしたい</li>
     <li>Select可能なテーブルにできるようにしたい　:point_left_tone3:次回はここ</li>
     <li>Formの要素として使いやすくしたい</li>
</ol>

参考になったり、ご指摘等あれば、いいね👍コメントよろしくお願いします。