家計簿アプリを作る #51:収支の一括削除
家計簿アプリ作成シリーズの第51回です。収支一覧で複数の取引をまとめて選択し、一括削除できるようにします。あわせて動作確認中に見つかった別の不具合も2件修正しました。
実装方針
各行にチェックボックスを追加し、選択したIDを $state で管理します。削除ボタン押下時に、選択中のID一覧をまとめてサーバーに送信します。
チェックボックスの選択状態は bind:group で管理する
flowbite-svelteの Checkbox はネイティブの <input type="checkbox"> をラップしているだけなので、onchange イベントで手動管理することもできますが、Svelteの bind:group を使えば大幅にシンプルになります。
<script lang="ts">
let selectedTransactionIds = $state<string[]>([]);
</script>
<Checkbox bind:group={selectedTransactionIds} value={transaction.id} />
チェックを入れると自動的に value が配列に追加され、外すと自動的に取り除かれます。onchange で手動実装すると、e.target の型キャストや追加/削除ロジックを自前で書く必要があり、コード量が増えるだけでした。
選択したIDをフォームで送信する
選択中のID配列を、同じ name の hidden inputとして並べて送信します。
<form method="POST" action="?/bulkDelete" use:enhance>
{#each selectedTransactionIds as id}
<input type="hidden" name="ids" value={id} />
{/each}
<Button type="submit">選択した項目を削除</Button>
</form>
サーバー側は FormData.getAll('ids') で、同じ name を持つ全ての値をまとめて配列として受け取れます。
const transactionIds = formData.getAll('ids') as string[];
if (transactionIds.length === 0) {
return fail(400, { error: 'At least one Transaction ID is required.' });
}
await db.delete(transactions)
.where(and(eq(transactions.userId, locals.user!.id), inArray(transactions.id, transactionIds)));
inArray(カラム, 配列) はSQLの IN に相当する条件で、drizzle-orm からインポートして使います。
ハマりポイント:カレンダーで指定した日付が1日ずれる
動作確認中、収支の登録・編集フォームでカレンダーから日付を選んでも、実際には1日前の日付で登録される不具合に気づきました。
原因は Date.prototype.toISOString() がUTC(協定世界時)に変換してから文字列化することです。JSTはUTC+9なので、日本時間の深夜0時付近の日付を選ぶと、UTC変換によって前日の日付にずれてしまいます。
const selectedDate = new Date(); // 例: 2026-07-23 00:00 JST
selectedDate.toISOString(); // "2026-07-22T15:00:00.000Z" ← 1日前になる
UTC変換を経由せず、ローカルタイムゾーンの年月日をそのまま組み立てる関数を用意して解決しました。登録・編集の2箇所で使うので、$lib/utils/date.ts に共通化しています。
// src/lib/utils/date.ts
export function formatLocalDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
ハマりポイント:チェックボックスのサイズが微妙に変わって見える
チェックボックスをクリックした直後と、別のチェックボックスに移動した後とで、大きさが微妙に変わって見える現象がありました。
原因はflowbite-svelteの Checkbox のデフォルトスタイルに含まれる focus:ring-2 でした。チェックボックス自体のサイズ(w-4 h-4)は変わっていませんが、フォーカスが当たっている間だけ周囲にリングが表示され、見た目上大きく見えます。別の要素にフォーカスが移るとリングが消えるため、元のサイズに戻ったように見えていました。
<!-- リングを無効化してサイズの見た目を統一する -->
<Checkbox bind:group={selectedTransactionIds} value={transaction.id} class="focus:ring-0" />
ただし、フォーカスリングはキーボード操作時に「今どこにフォーカスがあるか」を示すアクセシビリティ上の機能でもあるため、完全に消すことのトレードオフは意識しておく必要があります。
まとめ
| ポイント | 内容 |
|---|---|
| チェックボックスの選択管理 | bind:group を使えば追加/削除ロジックを自前で書かなくて済む |
| 複数IDの送信 | 同じnameのhidden inputを並べ、FormData.getAll()でまとめて取得する |
| 一括削除の条件 | inArray() でIDの配列に対するSQLのIN相当の条件を書く |
| 日付のタイムゾーンずれ | toISOString()はUTC変換されるため、ローカルの年月日を自前で組み立てる |
| フォーカスリングによる見た目の違い | focus:ring-2 がサイズの違いに見える原因。無効化はアクセシビリティとのトレードオフ |