家計簿アプリを作る #13:登録・編集フォームの改善
家計簿アプリ作成シリーズの第13回です。今回は収支の登録・編集フォームのカテゴリ入力をテキストから <select> に変更し、収入・支出で選択肢が切り替わるようにします。
実装方針
type(収入・支出)の選択に連動してカテゴリの選択肢を動的に切り替えます。Svelte 5 の $state と $derived を使います。
Step 1:カテゴリマップと状態を定義する
const categoryMap: Record<string, string[]> = {
income: ['給与', '副業', 'その他'],
expense: ['食費', '交通費', '日用品', '娯楽', 'その他']
};
let selectedType = $state('income');
let categories = $derived(categoryMap[selectedType] ?? []);
selectedType が変わると categories が自動的に再計算されます。
ハマりポイント①:categoryMap の型エラー
categoryMap の型を明示しないと以下のエラーが出ます。
Element implicitly has an 'any' type because expression of type 'string'
can't be used to index type '{ income: string[]; expense: string[]; }'.
Record<string, string[]> で型を明示することで解消できます。
const categoryMap: Record<string, string[]> = { ... };
Step 2:<select> を bind:value で連動させる
type の <select> に bind:value={selectedType} を追加します。
<select id="type" name="type" bind:value={selectedType}>
<option value="income">収入</option>
<option value="expense">支出</option>
</select>
カテゴリの <select> は categories をループします。
<select id="category" name="category">
{#each categories as category}
<option value={category}>{category}</option>
{/each}
</select>
編集フォームの対応
編集フォームでは既存データの type を初期値として渡す必要があります。
ハマりポイント②:$state の初期値に $props の値を使うとエラー
let { data, form } = $props();
let selectedType = $state(data.transaction.type); // ❌ エラー
This reference only captures the initial value of `data`.
Did you mean to reference it inside a derived instead?
let を const に変えることで解消できます。
const { data, form } = $props();
let selectedType = $state(data.transaction.type); // ✅
bind:value があれば selected 属性は不要
bind:value={selectedType} で選択状態が管理されるため、<option> の selected 属性は不要です。
<!-- ❌ 不要 -->
<option value="income" selected={data.transaction.type === 'income'}>収入</option>
<!-- ✅ bind:value に任せる -->
<option value="income">収入</option>
まとめ
| ポイント | 内容 |
|---|---|
| 動的な選択肢 | $state + $derived で type の変化に連動させる |
| 型エラー | categoryMap に Record<string, string[]> を明示する |
| 編集フォームの初期値 | $props() を const で受け取ることで $state の初期値に使える |
selected 属性 |
bind:value があれば不要 |
次回は収支の登録・編集後に元の月フィルタ状態に戻る処理を実装します。