コンテンツにスキップ

10-3. 更新・削除・入力チェックを追加する

  • 追加だけでなく、更新・削除でも state と画面表示をそろえる考え方
  • 入力チェックを「見た目の都合」ではなく「壊れたデータを入れない仕組み」として考える方法
  • エラーメッセージの表示場所を決め、ユーザーに何を直せばよいか伝える方法
  • render() を中心に、変更後の再描画を一貫させる方法

10-2 では、書籍一覧の初期表示とフォーム送信による追加を作った。
10-3 では、その画面を 「続けて使っても壊れにくい状態」 へ育てる。

具体的には、次の3つを追加する。

  • 不正な入力を保存前に止める
  • 読書状態を更新できるようにする
  • 削除後も件数表示や一覧表示がずれないようにする

このセクションの終わりでは、次の状態を目指す。

  • タイトルや著者が空のまま追加されない
  • 価格が 1 以上の整数でない場合はエラーになる
  • 一覧の「状態変更」ボタンで 未読 → 読書中 → 読了 と変えられる
  • 削除後も件数表示と一覧の行数が一致する
フォーム入力
↓ validateBook()
OK なら state を更新
render()
一覧・件数・エラー表示がそろう

10-3 も、完成コードを一気に入れるより、1ステップごとに保存 → ブラウザ更新 → 変化を確認する ほうが理解しやすい。

このセクションでは、各実装段階の末尾に「ブラウザを更新して確認する」を置く。毎回、次の順で進めよう。

  1. 指定された場所へコードを追加・修正する
  2. ファイルを保存する
  3. ブラウザを更新する
  4. 何が変わったか、変わらないならなぜ変わらないかを確認する

見た目が変わらないステップもある。
その場合は Console で state や関数の戻り値を見ながら、「まだ準備だけが終わった段階だ」と説明できることを目標にする。


1. なぜ入力チェックと状態更新が必要なのか

Section titled “1. なぜ入力チェックと状態更新が必要なのか”

10-2 の段階では「追加できる」ことが大切だった。
しかし実際のアプリでは、追加できるだけでは足りない。

  • タイトル空欄の本が入る
  • 価格が NaN のまま入る
  • 読み終わった本を 未読 のままにしてしまう
  • 削除後に件数表示だけ古いまま残る

こうした問題は、すべて state と画面表示のずれ として現れる。

つまり入力チェックは「厳しくするため」の処理ではなく、壊れた state を作らないための処理 である。


2. まずはエラー表示と操作列の置き場を決める

Section titled “2. まずはエラー表示と操作列の置き場を決める”

追加機能だけの段階では、一覧表に最低限の列だけあればよかった。
ここからは更新・削除を行うので、操作ボタンを置く場所が必要になる。

追加する場所: <form class="book-form"> タグの直前に <p id="form-error"> を 1 行追加する。テーブルは参照用(既存)。

index.html
<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 か所で表示する
  • 一覧の各行には「状態変更」「削除」の操作列を用意する

最初に表示場所を決めておくと、あとから「どこへ何を出すか」で迷いにくい。

  • 見た目はほとんど変わらなくてよい
  • form-error には hidden が付いているので、まだ画面へ出ない

この段階で大切なのは、エラーをどこへ出すかの置き場だけ先に決めた と説明できることである。


状態更新やエラー表示を扱うには、state と DOM 参照を少し増やす必要がある。

変更する場所: script.js の既存の state ブロックを削除し、緑の state ブロックへ置き換える。

script.js
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 ブロックへ置き換える。

script.js
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() でそろえやすくなる。

  • 見た目はまだ変わらない
  • Console で state.errorMessage を見ると "" になっている
  • Console で elements.errorelements.totalCount を見ると null ではないことを確認できる
  • もし null なら、HTML の id="form-error"id="total-count" と selector がずれている

この段階で参照のズレを見つけておくと、あとでエラー表示が出ないときに切り分けやすい。


まずは 1 冊分のデータをチェックする関数を作る。

追加する場所: script.js の末尾に追加する。

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] だけ表示してもよいが、設計としては「複数の不正をまとめて扱える」形にしておくと拡張しやすい。

  • 見た目はまだ変わらない
  • Console で validateBook({ title: "", author: "", category: "", price: 0, status: "未読", memo: "" }) を実行し、複数のエラー文言が返ることを確認する
  • 正しい値を渡したときは [] が返ることも確認する

ここでは、画面を変える前に、止める条件だけ先に作った と理解できればよい。


5. render() でエラー表示も一覧表示もまとめて更新する

Section titled “5. render() でエラー表示も一覧表示もまとめて更新する”

render() は「本の行を出す関数」ではなく、state を画面へ写す関数 だと考える。

変更する場所: 既存の render() を削除し、緑の render() へ置き換える。

script.js
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-actiondata-book-id を付ける。

script.js
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;
}
  • 一覧の各行に 状態変更削除 の 2 つのボタンが出る
  • state.errorMessage が空なら、フォーム近くのエラー文言はまだ表示されない
  • Console で state.errorMessage = "テスト"; render(); を実行すると文言が出て、state.errorMessage = ""; render(); で消える

ここで見たいのは、一覧とエラー表示の両方が render() 側の責務へ寄っていく ことである。


6. submit では「検証 → state 更新 → render」の順に進める

Section titled “6. submit では「検証 → state 更新 → render」の順に進める”

送信時の流れは次の順に固定すると分かりやすい。

  1. フォーム値を読む
  2. 検証する
  3. エラーなら state に反映して止める
  4. 問題なければ books を更新する
  5. render() を呼ぶ

変更する場所: 既存の submit イベントハンドラを削除し、緑のハンドラへ置き換える。

script.js
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 を入れる位置がずれると、不正入力なのに追加されるバグが起きやすい。
初学者はまず「先に止める条件を書く」と覚えると整理しやすい。

  1. タイトルを空欄にするか、価格に 0 を入れて送信する
  2. フォーム近くにエラーが表示され、一覧の行数や件数が増えないことを確認する
  3. 次に正しい値を入れて送信する
  4. エラーが消え、新しい本が先頭へ追加されることを確認する
  5. フォームが初期状態へ戻ることを確認する

ここでは、止める処理と通す処理が同じ submit の中で分かれている と分かればよい。


7. 状態変更と削除もイベント委譲で扱う

Section titled “7. 状態変更と削除もイベント委譲で扱う”

一覧の各行は render() のたびに作り直される。
そのため、各ボタンへ個別にイベントを付けるより、親要素でまとめて受けるほうが壊れにくい。

変更する場所: 既存の tbody click ハンドラを削除し、緑の nextStatus() と click ハンドラを追加する。

script.js
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 つを使い分けると、配列操作の意図が読みやすくなる。

  1. どれか 1 行の 状態変更 を押す
  2. 押した行だけが 未読 → 読書中 → 読了 と変わることを確認する
  3. 次に 削除 を押し、対象行だけが消えることを確認する
  4. 一覧の行数と件数表示が一致していることを確認する

ここで見たいのは、map()filter() の違いが画面の変化として分かるか である。


8. 「どこを変えると何が変わるか」を対応付ける

Section titled “8. 「どこを変えると何が変わるか」を対応付ける”

この段階で大事なのは、次の対応を頭の中で結び付けることである。

変えた場所何が変わるか
validateBook()不正入力を止める条件
state.errorMessageフォーム近くのエラー表示
state.books.unshift(...)追加後に先頭へ並ぶ
map() の中の status状態変更ボタンの結果
filter()削除後の行数と件数表示

これを文章で説明できるようになると、単にコードを写すのではなく、自分で責務を分けて考えられる ようになる。


  • validateBook() が配列ではなく文字列を返し、後で扱いにくくなる
  • エラー時に render() を呼ばず、画面へ何も出ない
  • map() の条件がずれて、全行の状態が変わる
  • 削除後に render() を呼ばず、画面だけ古いまま残る
1. submit / click イベントで console.log() を置く
2. validateBook() の戻り値を確認する
3. state.books の長さと中身を確認する
4. render() の呼び出しタイミングを確認する
5. 画面の行数と state.books.length が一致しているか比べる

特に初学者は、いきなり全部を見るより 「イベントは発火したか」「state は変わったか」「render は呼ばれたか」 の順で追うと整理しやすい。


観点ここで押さえたいこと
入力チェック壊れたデータを state へ入れる前に止める
エラー表示state.errorMessagerender() で画面へ反映する
状態変更map() で 1 件だけ更新する
削除filter() で残すデータを作り直す
再描画変更後は必ず render() を呼ぶ

演習問題 で、入力チェック・状態変更・削除の流れを自分の手で確認しよう。

その後は 10-4. Spring Bootで書籍APIの土台とCRUDを作る へ進み、今までブラウザ内で持っていたデータを、HTTP で扱える API へ広げていこう。