10-3. 更新・削除・入力チェックを追加する
このセクションで学ぶこと
Section titled “このセクションで学ぶこと”- 追加だけでなく、更新・削除でも
stateと画面表示をそろえる考え方 - 入力チェックを「見た目の都合」ではなく「壊れたデータを入れない仕組み」として考える方法
- エラーメッセージの表示場所を決め、ユーザーに何を直せばよいか伝える方法
render()を中心に、変更後の再描画を一貫させる方法
10-2 では、書籍一覧の初期表示とフォーム送信による追加を作った。
10-3 では、その画面を 「続けて使っても壊れにくい状態」 へ育てる。
具体的には、次の3つを追加する。
- 不正な入力を保存前に止める
- 読書状態を更新できるようにする
- 削除後も件数表示や一覧表示がずれないようにする
このセクションの終わりでは、次の状態を目指す。
- タイトルや著者が空のまま追加されない
- 価格が 1 以上の整数でない場合はエラーになる
- 一覧の「状態変更」ボタンで
未読 → 読書中 → 読了と変えられる - 削除後も件数表示と一覧の行数が一致する
フォーム入力 ↓ validateBook()OK なら state を更新 ↓render() ↓一覧・件数・エラー表示がそろうこのセクションの進め方
Section titled “このセクションの進め方”10-3 も、完成コードを一気に入れるより、1ステップごとに保存 → ブラウザ更新 → 変化を確認する ほうが理解しやすい。
このセクションでは、各実装段階の末尾に「ブラウザを更新して確認する」を置く。毎回、次の順で進めよう。
- 指定された場所へコードを追加・修正する
- ファイルを保存する
- ブラウザを更新する
- 何が変わったか、変わらないならなぜ変わらないかを確認する
見た目が変わらないステップもある。
その場合は Console で state や関数の戻り値を見ながら、「まだ準備だけが終わった段階だ」と説明できることを目標にする。
1. なぜ入力チェックと状態更新が必要なのか
Section titled “1. なぜ入力チェックと状態更新が必要なのか”10-2 の段階では「追加できる」ことが大切だった。
しかし実際のアプリでは、追加できるだけでは足りない。
- タイトル空欄の本が入る
- 価格が
NaNのまま入る - 読み終わった本を
未読のままにしてしまう - 削除後に件数表示だけ古いまま残る
こうした問題は、すべて state と画面表示のずれ として現れる。
つまり入力チェックは「厳しくするため」の処理ではなく、壊れた state を作らないための処理 である。
2. まずはエラー表示と操作列の置き場を決める
Section titled “2. まずはエラー表示と操作列の置き場を決める”追加機能だけの段階では、一覧表に最低限の列だけあればよかった。
ここからは更新・削除を行うので、操作ボタンを置く場所が必要になる。
追加する場所: <form class="book-form"> タグの直前に <p id="form-error"> を 1 行追加する。テーブルは参照用(既存)。
<p id="form-error" class="form-error" hidden></p>
<form class="book-form"> <div class="form-grid"> <label class="form-field" for="title"> <span>タイトル</span>ポイントは次の2つである。
- フォーム全体に対するエラーは、フォームの近くに 1 か所で表示する
- 一覧の各行には「状態変更」「削除」の操作列を用意する
最初に表示場所を決めておくと、あとから「どこへ何を出すか」で迷いにくい。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- 見た目はほとんど変わらなくてよい
form-errorにはhiddenが付いているので、まだ画面へ出ない
この段階で大切なのは、エラーをどこへ出すかの置き場だけ先に決めた と説明できることである。
3. state と elements を拡張する
Section titled “3. state と elements を拡張する”状態更新やエラー表示を扱うには、state と DOM 参照を少し増やす必要がある。
変更する場所: script.js の既存の state ブロックを削除し、緑の state ブロックへ置き換える。
const state = { books: [ { id: "b1", title: "JavaScript入門", author: "山田太郎", category: "フロントエンド", price: 2800, status: "未読", memo: "DOM 操作の章まで読む", }, { id: "b2", title: "Git実践入門", author: "佐藤花子", category: "開発ツール", price: 2400, status: "読了", memo: "ブランチ運用を復習済み", }, ],};
const state = { books: [ { id: "b1", title: "JavaScript入門", author: "山田太郎", category: "フロントエンド", price: 2800, status: "未読", memo: "DOM 操作の章まで読む", }, ], errorMessage: "",};変更する場所: script.js の既存の elements ブロックを削除し、緑の elements ブロックへ置き換える。
const elements = { form: document.querySelector("#book-form"), title: document.querySelector("#title"), author: document.querySelector("#author"), category: document.querySelector("#category"), price: document.querySelector("#price"), status: document.querySelector("#status"), memo: document.querySelector("#memo"), tbody: document.querySelector("#book-table-body"), emptyMessage: document.querySelector("#empty-message"), totalCount: document.querySelector("#total-count"), readingCount: document.querySelector("#reading-count"), finishedCount: document.querySelector("#finished-count"),};
const elements = { form: document.querySelector("#book-form"), title: document.querySelector("#title"), author: document.querySelector("#author"), category: document.querySelector("#category"), price: document.querySelector("#price"), status: document.querySelector("#status"), memo: document.querySelector("#memo"), error: document.querySelector("#form-error"), tbody: document.querySelector("#book-table-body"), emptyMessage: document.querySelector("#empty-message"), totalCount: document.querySelector("#total-count"), readingCount: document.querySelector("#reading-count"), finishedCount: document.querySelector("#finished-count"),};10-2 で追加した集計カードの #total-count / #reading-count / #finished-count は、この章でもそのまま使う。
ここでは新しく #count を作るのではなく、既存の集計表示へエラー表示の参照だけを足す と考えるとつながりが分かりやすい。
ここで大切なのは、エラーも state の一部として扱う という考え方である。
その場しのぎで alert() を出すより、「今の画面が何を伝えるべきか」を render() でそろえやすくなる。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- 見た目はまだ変わらない
- Console で
state.errorMessageを見ると""になっている - Console で
elements.errorとelements.totalCountを見るとnullではないことを確認できる - もし
nullなら、HTML のid="form-error"やid="total-count"と selector がずれている
この段階で参照のズレを見つけておくと、あとでエラー表示が出ないときに切り分けやすい。
4. 保存前に入力を検証する
Section titled “4. 保存前に入力を検証する”まずは 1 冊分のデータをチェックする関数を作る。
追加する場所: script.js の末尾に追加する。
function validateBook(book) { const errors = [];
if (!book.title) { errors.push("タイトルは必須です"); }
if (!book.author) { errors.push("著者は必須です"); }
if (!book.category) { errors.push("カテゴリを選択してください"); }
if (!Number.isInteger(book.price) || book.price <= 0) { errors.push("価格は1以上の整数で入力してください"); }
if (!["未読", "読書中", "読了"].includes(book.status)) { errors.push("状態の値が不正です"); }
if (book.memo.length > 120) { errors.push("メモは120文字以内です"); }
return errors;}配列で返す理由は、エラーが 1 個とは限らないからである。
最初の学習段階では errors[0] だけ表示してもよいが、設計としては「複数の不正をまとめて扱える」形にしておくと拡張しやすい。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- 見た目はまだ変わらない
- Console で
validateBook({ title: "", author: "", category: "", price: 0, status: "未読", memo: "" })を実行し、複数のエラー文言が返ることを確認する - 正しい値を渡したときは
[]が返ることも確認する
ここでは、画面を変える前に、止める条件だけ先に作った と理解できればよい。
5. render() でエラー表示も一覧表示もまとめて更新する
Section titled “5. render() でエラー表示も一覧表示もまとめて更新する”render() は「本の行を出す関数」ではなく、state を画面へ写す関数 だと考える。
変更する場所: 既存の render() を削除し、緑の render() へ置き換える。
function render() { renderSummary(); renderBooks();}
function render() { if (state.errorMessage) { elements.error.hidden = false; elements.error.textContent = state.errorMessage; } else { elements.error.hidden = true; elements.error.textContent = ""; }
renderSummary(); renderBooks();}件数と一覧は 10-2 の renderSummary() / renderBooks() をそのまま使い、10-3 ではそこへエラー表示の更新を足している。
この形にすると、「件数だけ変わった」「エラーだけ残った」といった中途半端な状態を減らしやすい。
あわせて操作列のボタンも更新する
Section titled “あわせて操作列のボタンも更新する”7 では data-action でクリック内容を判定するので、操作列を描画する関数も 2 つのボタンを返す形へ変えておく。
変更する場所: 既存の createActionCell() を削除し、緑の createActionCell() へ置き換える。renderRow() で直接操作列を作っている場合も、同じ考え方で data-action と data-book-id を付ける。
function createActionCell(bookId) { const td = document.createElement("td"); const button = document.createElement("button"); button.type = "button"; button.className = "ghost-button delete-button"; button.dataset.bookId = bookId; button.textContent = "削除"; td.appendChild(button); return td;}
function createActionCell(bookId) { const td = document.createElement("td");
const toggleButton = document.createElement("button"); toggleButton.type = "button"; toggleButton.className = "ghost-button"; toggleButton.dataset.action = "toggle-status"; toggleButton.dataset.bookId = bookId; toggleButton.textContent = "状態変更";
const deleteButton = document.createElement("button"); deleteButton.type = "button"; deleteButton.className = "ghost-button delete-button"; deleteButton.dataset.action = "delete"; deleteButton.dataset.bookId = bookId; deleteButton.textContent = "削除";
td.append(toggleButton, " ", deleteButton); return td;}ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- 一覧の各行に
状態変更と削除の 2 つのボタンが出る state.errorMessageが空なら、フォーム近くのエラー文言はまだ表示されない- Console で
state.errorMessage = "テスト"; render();を実行すると文言が出て、state.errorMessage = ""; render();で消える
ここで見たいのは、一覧とエラー表示の両方が render() 側の責務へ寄っていく ことである。
6. submit では「検証 → state 更新 → render」の順に進める
Section titled “6. submit では「検証 → state 更新 → render」の順に進める”送信時の流れは次の順に固定すると分かりやすい。
- フォーム値を読む
- 検証する
- エラーなら state に反映して止める
- 問題なければ books を更新する
render()を呼ぶ
変更する場所: 既存の submit イベントハンドラを削除し、緑のハンドラへ置き換える。
elements.form.addEventListener("submit", (event) => { event.preventDefault();
const formData = readBookFormData(); const newBook = createBook(formData);
state.books.unshift(newBook); render(); resetForm();});
elements.form.addEventListener("submit", (event) => { event.preventDefault();
const formData = readBookFormData(); const errors = validateBook(formData);
if (errors.length > 0) { state.errorMessage = errors[0]; render(); return; }
state.errorMessage = ""; state.books.unshift({ id: "b" + Date.now(), ...formData, }); render(); elements.form.reset();});return を入れる位置がずれると、不正入力なのに追加されるバグが起きやすい。
初学者はまず「先に止める条件を書く」と覚えると整理しやすい。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- タイトルを空欄にするか、価格に
0を入れて送信する - フォーム近くにエラーが表示され、一覧の行数や件数が増えないことを確認する
- 次に正しい値を入れて送信する
- エラーが消え、新しい本が先頭へ追加されることを確認する
- フォームが初期状態へ戻ることを確認する
ここでは、止める処理と通す処理が同じ submit の中で分かれている と分かればよい。
7. 状態変更と削除もイベント委譲で扱う
Section titled “7. 状態変更と削除もイベント委譲で扱う”一覧の各行は render() のたびに作り直される。
そのため、各ボタンへ個別にイベントを付けるより、親要素でまとめて受けるほうが壊れにくい。
変更する場所: 既存の tbody click ハンドラを削除し、緑の nextStatus() と click ハンドラを追加する。
elements.tbody.addEventListener("click", (event) => { const target = event.target;
if (!(target instanceof HTMLElement)) { return; }
if (!target.matches(".delete-button")) { return; }
const bookId = target.dataset.bookId; state.books = state.books.filter((book) => book.id !== bookId); render();});
function nextStatus(currentStatus) { const statuses = ["未読", "読書中", "読了"]; const currentIndex = statuses.indexOf(currentStatus); return statuses[(currentIndex + 1) % statuses.length];}
elements.tbody.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return;
const actionButton = target.closest("button[data-action]"); if (!(actionButton instanceof HTMLButtonElement)) return;
const bookId = actionButton.dataset.bookId; const action = actionButton.dataset.action;
if (action === "toggle-status") { state.books = state.books.map((book) => book.id === bookId ? { ...book, status: nextStatus(book.status) } : book ); render(); return; }
if (action === "delete") { state.books = state.books.filter((book) => book.id !== bookId); render(); }});- 状態変更は
map()で「1冊だけ中身を変える」 - 削除は
filter()で「残す本だけを集め直す」
この 2 つを使い分けると、配列操作の意図が読みやすくなる。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- どれか 1 行の
状態変更を押す - 押した行だけが
未読 → 読書中 → 読了と変わることを確認する - 次に
削除を押し、対象行だけが消えることを確認する - 一覧の行数と件数表示が一致していることを確認する
ここで見たいのは、map() と filter() の違いが画面の変化として分かるか である。
8. 「どこを変えると何が変わるか」を対応付ける
Section titled “8. 「どこを変えると何が変わるか」を対応付ける”この段階で大事なのは、次の対応を頭の中で結び付けることである。
| 変えた場所 | 何が変わるか |
|---|---|
validateBook() | 不正入力を止める条件 |
state.errorMessage | フォーム近くのエラー表示 |
state.books.unshift(...) | 追加後に先頭へ並ぶ |
map() の中の status | 状態変更ボタンの結果 |
filter() | 削除後の行数と件数表示 |
これを文章で説明できるようになると、単にコードを写すのではなく、自分で責務を分けて考えられる ようになる。
よくある失敗
Section titled “よくある失敗”validateBook()が配列ではなく文字列を返し、後で扱いにくくなる- エラー時に
render()を呼ばず、画面へ何も出ない map()の条件がずれて、全行の状態が変わる- 削除後に
render()を呼ばず、画面だけ古いまま残る
1. submit / click イベントで console.log() を置く2. validateBook() の戻り値を確認する3. state.books の長さと中身を確認する4. render() の呼び出しタイミングを確認する5. 画面の行数と state.books.length が一致しているか比べる特に初学者は、いきなり全部を見るより 「イベントは発火したか」「state は変わったか」「render は呼ばれたか」 の順で追うと整理しやすい。
| 観点 | ここで押さえたいこと |
|---|---|
| 入力チェック | 壊れたデータを state へ入れる前に止める |
| エラー表示 | state.errorMessage と render() で画面へ反映する |
| 状態変更 | map() で 1 件だけ更新する |
| 削除 | filter() で残すデータを作り直す |
| 再描画 | 変更後は必ず render() を呼ぶ |
次のステップ
Section titled “次のステップ”演習問題 で、入力チェック・状態変更・削除の流れを自分の手で確認しよう。
その後は 10-4. Spring Bootで書籍APIの土台とCRUDを作る へ進み、今までブラウザ内で持っていたデータを、HTTP で扱える API へ広げていこう。