家計簿アプリを作る #5:収支の削除を実装する

家計簿アプリ作成シリーズの第5回です。今回は一覧をテーブル表示に変更し、各行に削除ボタンを追加します。

一覧をテーブル表示に変更する

削除ボタンを配置しやすくするため、+page.svelte の一覧表示を <ul> から <table> に変更します。最後の列を削除ボタン用のスペースとして確保します。

+page.svelte:削除ボタンを追加する

<script lang="ts">
  import { enhance } from "$app/forms";

  let { data, form } = $props();
</script>

<table>
  <thead>
    <tr>
      <th>日付</th>
      <th>種類</th>
      <th>カテゴリ</th>
      <th>メモ</th>
      <th>金額</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    {#each data.transactions as transaction}
      <tr>
        <td>{transaction.date}</td>
        <td>{transaction.type}</td>
        <td>{transaction.category}</td>
        <td>{transaction.memo}</td>
        <td>{transaction.amount}</td>
        <td>
          <form method="POST" action="?/delete" use:enhance>
            <input type="hidden" name="id" value={transaction.id} />
            <button type="submit">削除</button>
          </form>
        </td>
      </tr>
    {/each}
  </tbody>
</table>

{#if form?.error}
  <p style="color: red">{form.error}</p>
{/if}

ハマりポイント:<form> の位置

最初、<form> でテーブル全体を囲む実装にしていました。

<!-- ❌ テーブル全体を囲むと全行の id が送信される -->
<form method="POST" action="?/delete" use:enhance>
  <table>
    {#each data.transactions as transaction}
      <tr>
        ...
        <input type="hidden" name="id" value={transaction.id} />
        <button type="submit">削除</button>
      </tr>
    {/each}
  </table>
</form>

この場合、すべての行の hidden input が一括で送信されるため、formData.get('id') では先頭の行の id しか取れません。

解決策は <form> を各行の <td> の中に移動することです。<form><tr><tbody> の直下に置くと HTML 的に無効になるため、必ず <td> の中に入れます。

<!-- ✅ <form> を各行の <td> 内に配置 -->
<td>
  <form method="POST" action="?/delete" use:enhance>
    <input type="hidden" name="id" value={transaction.id} />
    <button type="submit">削除</button>
  </form>
</td>

エラー表示の位置

<form> が各行に分散しているため、form?.error はテーブルの外に置きます。form$props() から受け取っているので <form> タグの外でも参照できます。

{#if form?.error}
  <p style="color: red">{form.error}</p>
{/if}

+page.server.ts:delete アクションを追加する

import { fail, redirect } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { createDb } from '$lib/server/db';
import { transactions } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';

export const load: PageServerLoad = async ({ platform }) => {
  const db = createDb(platform!.env.DB);
  const allTransactions = await db.select().from(transactions);
  return { transactions: allTransactions };
};

export const actions: Actions = {
  delete: async ({ request, platform }) => {
    const formData = await request.formData();
    const id = formData.get('id');

    if (!id) {
      return fail(400, { error: 'Transaction ID is required.' });
    }

    const db = createDb(platform!.env.DB);
    await db.delete(transactions).where(eq(transactions.id, String(id)));

    redirect(303, '/');
  }
};

ポイント:

  • Drizzle の eq()drizzle-orm からインポートして WHERE 条件に使う
  • formData.get('id')FormDataEntryValue | null なので String() で変換してから渡す

まとめ

ポイント 内容
<form> の位置 テーブル全体を囲まず、各行の <td> 内に配置する
hidden input 削除対象の id を各行の <form> 内で送信する
エラー表示 form$props() 由来なので <form> タグ外でも参照できる
WHERE 条件 eq()drizzle-orm からインポートして使う

次回は収支の編集を実装します。

← トップページに戻る