家計簿アプリを作る #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 からインポートして使う |
次回は収支の編集を実装します。