SvelteKit Skeleton Datatable使いやすくしてみた ~Selectableにする~

# はじめに
: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>データを渡すだけでテーブル表示するコンポーネントにしたい</li>
     <li>Select可能なテーブルにできるようにしたい:point_left_tone3:今ここ</li>
     <li>Formの要素として使いやすくしたい</li>
</ol>

part1「データを渡すだけでテーブル表示するコンポーネントにしたい」は下記の記事になります。

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

現在Datatableコンポーネントは以下のようになっています。
/lib/components/Datatable.svelte
```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;}
</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">
        {#if searchFields.length > 0}
            {#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:「今は、Datatableコンポーネントにデータを渡したら、画面でいい感じに表示できているけど、そこから先は何もできていないよね。」

:robot:「今回は、行をクリックしたら、選択状態にして、コンポーネントのインポート先で選択状態の行のデータを受け取れるようにしていきたいと思います。」
# 2.Select可能なテーブルにできるようにしたい
### 行選択可能にするやり方
:robot: 「Skeletonの公式でTailWindのデータテーブルの説明に行選択の方法がありました。」

https://www.skeleton.dev/elements/tables

:robot:「まずは、選択中の行データを保持する変数`selectedRow`とデータをセットするメソッド`selectRow`を用意します」
Datatable.svelte
```diff_svelte
...
<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();
+   let selectedRow: RowData | null = null;

+   function selectRow(row: RowData) {
+       selectedRow = row;
+   }
...
</script>
```
:robot:「次に行オブジェクトのクリック時に、`selectRow`を実行し、rowオブジェクトと、selectedRowが同じな時、`table-row-checkedクラスを付与します。」
Datatable.svlete
```diff_svelte
...
        <tbody>
            {#each $rows as row}
                <tr 
+                  on:click={() => selectRow(row)}
+                  class="{selectedRow === row ? "table-row-checked" : ''}"
                >
                    {#each columns as column}
                        {#if !column.hidden}
                            <td>{row[column.key]}</td>
                        {/if}
                    {/each}
                </tr>
            {/each}
        </tbody>
    </table>
...
```
:baby:「見た目だけでいうと、選択中の行には、`table-row-checked`というクラスを与えればいいんだね！」

### インポート先で選択された行データを受け取る
:robot:「次は、Datatable.svelteのインポート先の+page.svelteでデータを表示してみましょう」
　　「まずは、先ほど作成した`selectRow`メソッドで'selected'イベントをdispatchします。」

:baby:　「子→親への値の受け渡しは、dispatchすればよかったね！！」

```diff_svelte
<script lang="ts">
...

+   import { createEventDispatcher } from 'svelte';
+   const dispatch = createEventDispatcher();

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

    const handler = new DataHandler(data, { rowsPerPage: 5 });
    const rows = handler.getRows();
    let selectedRow: RowData | null = null;

    function selectRow(row: RowData) {
        selectedRow = row;
+       dispatch('selected',{
+           value: selectedRow
+       });
    }
```

:robot:「次に、+page.svelteでイベントを受け取り表示します。」
```diff_svelte
<script lang="ts">
    import type { UserInterface } from './userInterface.js';
    import Datatable, { type ColumnData, type RowData, type SearchField } from '$lib/components/Datatable.svelte';
    
    export let data: {userList: UserInterface[]};
    
    let userList: UserInterface[] = [];
    userList = data.userList;
    
    const columns:ColumnData[] = [
       {name:"ユーザーID", key:"user_id", hidden:true},
       {name:"名前", key:"username"},
       {name:"Email", key:"email"},
       {name:"部署C", key:"deptC"}
    ];

    const searchFields: SearchField[] = [
        {keys:["username","email"], placeholder:"名前またはEmailで検索"},
        {keys:["user_id"], placeholder:"user_idで検索"}
    ]

+   let selectedRow:RowData | null = null;
+   function handleSelectedRow(event: { detail: { value: RowData | null; }; }){
+       selectedRow = event.detail.value;
+   }
</script>
<h1>ユーザー一覧</h1>
<div style="width: 70%;">
    <Datatable
        data={userList}
        columns={columns}
        searchFields={searchFields}
+       on:selected={handleSelectedRow}
    />
</div>
+<div class="bg-surface-400">
+    {#if selectedRow}
+        <h3>選択された行のデータ:</h3>
+        <ul>
+            {#each columns as column}
+                <li><strong>{column.key}:</strong> {selectedRow[column.key]}</li>
+            {/each}
+        </ul>
+    {:else}
+        <p>行を選択してください。</p>
+    {/if}
</div>
```
# 実行結果
![画面収録 2024-09-09 0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3484672/a534375e-c9da-d2c0-f842-9d5c94f40d73.gif)

:baby:「クリックしたら、色が変わって、選択した行データの値もインポート先で受け取れてるね！！」

# 終わりに
いよいよコンポーネントとして使えそうな感じになってきました。
もしわからないところや、もっとこうした方がいいよ！があれば、コメントお願いします。
※基本は公式を読むのが吉です。

次回はいよいよ「3.Formの要素として使いやすくしたい」です。