家計簿アプリを作る #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 がサイズの違いに見える原因。無効化はアクセシビリティとのトレードオフ
← トップページに戻る