10-2. フロントエンドで一覧表示とフォーム入力を作る
このセクションで学ぶこと
Section titled “このセクションで学ぶこと”- 書籍データを JavaScript の配列・オブジェクトで持つ考え方
querySelector/addEventListenerを使って画面とコードをつなぐ方法- フォーム送信、一覧表示、削除を DOM 操作で実現する手順
stateを更新してからrender()する流れ- Console / Elements を使って、どこで値がずれたかを調べる方法
前のセクションでは、書籍管理アプリの見た目だけを作った。
ここからは JavaScript を加えて、入力 → 画面更新 の流れをブラウザの中で動かしていく。
このセクションの終わりでは、次の動きができる状態を目指す。
- フォームから本を 1 冊追加できる
- 一覧に新しい行が表示される
- 登録冊数・読書中・読了の件数が更新される
- 削除ボタンで一覧から消せる
- まだ保存はしないので、ページを再読み込みすると元に戻る
フォーム入力 ↓ submitstate.books に 1 件追加 ↓render() ├─ 集計カードを書き換える ├─ tbody を作り直す └─ 空メッセージの表示/非表示を切り替えるこのセクションの進め方
Section titled “このセクションの進め方”10-2 は、完成コードを一気に貼り付けるよりも、1ステップごとに保存 → ブラウザ更新 → 変化を確認する ほうが理解しやすい。
このセクションでは、各実装段階の末尾に「ブラウザを更新して確認する」を置く。毎回、次の順で進めよう。
- 指定された場所へコードを追加・修正する
- ファイルを保存する
- ブラウザを更新する
- 何が変わったか、変わらないならなぜ変わらないかを確認する
見た目が変わらないステップもある。
その場合も「まだ state を DOM へ写していないから変わらない」と説明できることが、DOM 操作の理解につながる。
1. JavaScript で画面を動かすときの考え方
Section titled “1. JavaScript で画面を動かすときの考え方”初心者がつまずきやすいのは、「どのタイミングで画面が変わるのか」が頭の中で曖昧なことだ。
書籍管理アプリでは、次の流れで考えると整理しやすい。
ユーザーが操作する ↓JavaScript が状態(state)を更新する ↓render() で DOM を描き直すこの順番が大切である。
- DOM をその場しのぎで直接書き換えるだけだと、データの正しい姿が分からなくなる
- 先に
stateを更新しておけば、「今の正しいデータは何か」が 1 か所で分かる
つまり JavaScript では、画面そのものより、画面の元になるデータを先に考える のが基本である。
2. まず HTML に JavaScript 用の目印を付ける
Section titled “2. まず HTML に JavaScript 用の目印を付ける”前のセクションの HTML に、JavaScript から触りたい場所の id を追加する。
変更する場所: index.html の各要素を修正する。赤い行(-)を緑の行(+)に書き換える。緑だけの行はコード内のコメントが示す位置に追加する。
まず、書籍登録パネルの <h2> を section-heading div で包み、注釈を加える。
<section class="panel"><!-- すでにある(書籍を登録するパネル) --> <h2>書籍を登録する</h2> <div class="section-heading"> <h2>書籍を登録する</h2> <p class="section-note">JavaScript で表示を更新します</p> </div>
<form class="book-form"><!-- 続き、すでにある -->次に、集計カードの id 付け・フォームの id 付け・一覧パネルの空メッセージ追加・tbody 差し替え・script タグ追加をまとめて行う。
<section class="summary-grid" aria-label="現在の集計"> <article class="summary-card"> <p class="summary-label">登録冊数</p> <p class="summary-value">3</p> <p class="summary-value" id="total-count">0</p> </article> <article class="summary-card"> <p class="summary-label">読書中</p> <p class="summary-value">1</p> <p class="summary-value" id="reading-count">0</p> </article> <article class="summary-card"> <p class="summary-label">読了</p> <p class="summary-value">1</p> <p class="summary-value" id="finished-count">0</p> </article></section>
<form class="book-form"><form class="book-form" id="book-form"> <!-- 入力欄は前節と同じ。id は title / author / category / price / status / memo を使う --> <button class="primary-button" type="submit">書籍を追加</button> <button class="primary-button" id="submit-button" type="submit">書籍を追加</button></form>
<section class="panel"> <div class="section-heading"> <h2>登録済み書籍</h2> <p class="section-note">この段階ではまだサンプル表示です</p> </div><!-- section-heading の閉じタグ(すでにある) -->
<p class="empty-message" id="empty-message">まだ書籍はありません。</p><!-- ↑ ここに追加 -->
<div class="table-wrapper"><!-- すでにある --> <table class="book-table"> <thead> <tr> <th>タイトル</th> <th>著者</th> <th>カテゴリ</th> <th>価格</th> <th>状態</th> <th>メモ</th> <th>操作</th> </tr> </thead> <tbody id="book-table-body"> <!-- 前節のサンプル行 2 件をすべて削除する --> </tbody> <tbody id="book-table-body"></tbody> </table> </div></section><!-- 一覧パネルの閉じタグ(すでにある) -->
<script src="./script.js" defer></script><!-- ↑ ここに追加 -->このステップで <script src="./script.js" defer></script> も追加するので、更新前に空の script.js を 1 つ作っておくと、Console の 404 エラーを避けられる。
中身はまだ空でよい。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- 前のセクションで置いたサンプル行が消え、「まだ書籍はありません。」が見える
- 集計カードの数字は
0 / 0 / 0になっている(HTML を書き換えたため) - つまりこの段階では、JavaScript が書き換える置き場だけ先に用意した 状態である
一覧が空で集計も 0 なのは、まだ JavaScript が動いていないからである。
次の render() で、初めて state の内容が画面表示に反映される。
なぜ id を付けるのか
Section titled “なぜ id を付けるのか”document.querySelector('#book-form') のように、JavaScript は CSS セレクタで要素を探す。
そのため、
- フォーム本体
- 件数表示
- テーブル本体
- 空メッセージ
のような「コードから触る場所」は、名前を付けておく必要がある。
3. 状態(state)を 1 つのオブジェクトで持つ
Section titled “3. 状態(state)を 1 つのオブジェクトで持つ”次に script.js を作る。
book-app/└── book-frontend/ ├── index.html ├── style.css └── script.js最初に、アプリのデータを 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: 'ブランチ運用を復習済み', }, ],};なぜ配列の中にオブジェクトを入れるのか
Section titled “なぜ配列の中にオブジェクトを入れるのか”1 冊の本には、タイトルだけでなく著者や価格もある。
そこで 1 冊を オブジェクト で表し、それを複数冊まとめて 配列 に入れる。
books├─ 1冊目 { title, author, category, ... }├─ 2冊目 { title, author, category, ... }└─ 3冊目 { title, author, category, ... }この形にしておくと、追加・削除・集計がやりやすい。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- 見た目は前のステップから変わらなくてよい
- まだ
stateを作っただけで、画面へ描き出す処理は書いていないからである - Console で
state.books.lengthを実行すると2になる
「データはあるが、まだ画面へは出していない」という状態を意識できると、DOM 操作で迷いにくくなる。
4. DOM 要素を取得する
Section titled “4. DOM 要素を取得する”次に、HTML の部品を JavaScript 側で取り出す。
追加する場所: script.js の state 定義の下に追加する。
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'),};ここで null になってしまう場合は、HTML 側の id が違っている可能性が高い。
つまり、JavaScript のエラーはコードの文法ミスだけでなく、HTML との約束のズレ でも起きる。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- 見た目はまだ変わらない
- ただし Console で
elements.formやelements.tbodyを見ると、nullではないことを確認できる - もし
nullなら、HTML のidと JavaScript の selector がずれている
この段階で参照のズレを見つけておくと、あとで render() が動かないときに原因を切り分けやすい。
5. フォームの値を 1 冊分のデータへ変換する
Section titled “5. フォームの値を 1 冊分のデータへ変換する”入力欄はすべて文字列として読まれる。
ただし価格だけは数値として扱いたいので、Number() で変換する。
追加する場所: script.js の elements 定義の下に追加する。
function readBookFormData() { return { title: elements.title.value.trim(), author: elements.author.value.trim(), category: elements.category.value, price: Number(elements.price.value), status: elements.status.value, memo: elements.memo.value.trim(), };}
function createBook(formData) { return { id: String(Date.now()), title: formData.title, author: formData.author, category: formData.category, price: formData.price, status: formData.status, memo: formData.memo, };}なぜ trim() を使うのか
Section titled “なぜ trim() を使うのか”"JavaScript入門" と " JavaScript入門 " は、人には同じに見えても文字列としては別物である。
前後の空白を消しておくと、あとで検索や比較をするときに無駄なズレが減る。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- 見た目はまだ変わらない
- Console で
readBookFormData()を実行すると、今フォームに入っている値が 1 冊分のオブジェクトとして返る priceだけが文字列ではなく数値になっていることを確認する
ここで「入力欄の値をどういう形で受け取るのか」を確かめておくと、submit の処理を理解しやすい。
6. render() で画面を描き直す
Section titled “6. render() で画面を描き直す”6-1. 件数を更新する
Section titled “6-1. 件数を更新する”追加する場所: script.js の末尾に追加する。
function renderSummary() { const total = state.books.length; const reading = state.books.filter((book) => book.status === '読書中').length; const finished = state.books.filter((book) => book.status === '読了').length;
elements.totalCount.textContent = String(total); elements.readingCount.textContent = String(reading); elements.finishedCount.textContent = String(finished);}この時点では関数を追加しただけなので、まだ画面は変わらない。
続けて 6-2 と 6-3 まで入れてからブラウザを更新しよう。
6-2. 一覧テーブルを更新する
Section titled “6-2. 一覧テーブルを更新する”今回は textContent を使って安全にセルを作る。
この書き方なら、入力値がそのまま HTML として解釈されにくい。
追加する場所: script.js の末尾に追加する。
function getStatusClass(status) { if (status === '読書中') return 'status-reading'; if (status === '読了') return 'status-finished'; return 'status-unread';}
function createTextCell(text) { const td = document.createElement('td'); td.textContent = text; return td;}
function createStatusCell(status) { const td = document.createElement('td'); const badge = document.createElement('span'); badge.className = `status-badge ${getStatusClass(status)}`; badge.textContent = status; td.appendChild(badge); return td;}
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 renderBooks() { elements.tbody.innerHTML = '';
elements.emptyMessage.hidden = state.books.length > 0;
state.books.forEach((book) => { const tr = document.createElement('tr');
tr.appendChild(createTextCell(book.title)); tr.appendChild(createTextCell(book.author)); tr.appendChild(createTextCell(book.category)); tr.appendChild(createTextCell(`${book.price}円`)); tr.appendChild(createStatusCell(book.status)); tr.appendChild(createTextCell(book.memo || '—')); tr.appendChild(createActionCell(book.id));
elements.tbody.appendChild(tr); });}
function render() { renderSummary(); renderBooks();}data-book-id と dataset.bookId の関係
Section titled “data-book-id と dataset.bookId の関係”HTML の data-* 属性は、JavaScript では dataset から読める。たとえば data-book-id="b001" は element.dataset.bookId で取得する。ハイフン区切りが camelCase に変わる点が、最初につまずきやすいポイントである。
state と DOM の関係を図で見る
Section titled “state と DOM の関係を図で見る”state.books が正しいデータ ↓renderSummary() が件数へ変換renderBooks() が table 行へ変換 ↓画面に見える文字やボタンが更新されるこの形にしておくと、表示が崩れたときも「state が変なのか」「render が変なのか」を分けて調べやすい。
ここまでは関数の準備だけで、まだ render() を実行していない。
次の 6-3 で最初の 1 回を呼び出すと、初めてブラウザの表示が変わる。
6-3. 最初の描画を呼び出す
Section titled “6-3. 最初の描画を呼び出す”関数を定義しただけでは、state.books は画面へ出ない。
最初の 1 回だけ render() を呼び、初期データを DOM へ写す。
追加する場所: script.js の末尾に追加する。
render();これを忘れると、state.books にデータがあっても画面は空のままである。
データを持っていること と 画面へ出ていること は別だ、という点は DOM 操作で非常に重要である。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- 一覧に
JavaScript入門とGit実践入門の 2 行が出る - 集計カードが
2 / 0 / 1に変わる まだ書籍はありません。は非表示になる
ここで初めて、「state の内容が render() を通じて DOM へ写る」感覚が見え始める。
7. フォーム送信で 1 冊追加する
Section titled “7. フォーム送信で 1 冊追加する”フォーム送信時には、ブラウザのデフォルト動作でページ再読み込みが起きる。
今回は JavaScript だけで画面を更新したいので、preventDefault() で止める。
追加する場所: script.js の末尾に追加する。
function resetForm() { elements.form.reset(); elements.status.value = '未読'; elements.title.focus();}
elements.form.addEventListener('submit', (event) => { event.preventDefault();
const formData = readBookFormData(); const newBook = createBook(formData);
state.books.unshift(newBook); render(); resetForm();});なぜ「先に state を更新する」のか
Section titled “なぜ「先に state を更新する」のか”悪い例は、送信イベントの中でその場で tr を作り、件数も別で増やし、空メッセージも別で消す書き方である。
それだと処理がばらけて、どこか 1 か所だけ更新漏れしやすい。
今の書き方では、
state.booksに新しいデータを追加するrender()を 1 回呼ぶ
だけで、一覧も件数もまとめて更新される。
これが 状態駆動で画面を描く という考え方の入口である。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新し、初期の 2 行が表示されることを確認する
- フォームへ 1 冊分の情報を入力して「書籍を追加」を押す
- 新しい本が先頭行に追加されることを確認する
- 登録冊数と状態別の件数が更新されることを確認する
- フォームが空に戻り、タイトル入力欄へフォーカスが戻ることを確認する
ここでは「保存して更新したあと、クリックや送信で画面が変わるか」を見る。
読み込み直後の見た目と、操作後の見た目を分けて観察するのがポイントである。
8. 削除ボタンで 1 冊消す
Section titled “8. 削除ボタンで 1 冊消す”行ごとに個別のイベントを付ける方法もあるが、今回は tbody に 1 つだけ 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();});instanceof HTMLElement と matches() は何をしているか
Section titled “instanceof HTMLElement と matches() は何をしているか”event.target はクリックされた対象を返すが、型としては広く、テキストノードのように HTMLElement ではない値が来ることもある。そこで instanceof HTMLElement を使い、「HTML 要素として安全に扱えるか」を最初に確認している。
matches('.delete-button') は、その要素自身が .delete-button に一致するかを調べる。イベント委譲では tbody 全体で click を受けるため、「本当に削除ボタンが押されたか」をここで絞り込む必要がある。
target.dataset.bookId は、ボタンへ埋め込んだ data-book-id を読み取り、「どの書籍を削除するか」を特定するための値である。
なぜイベント委譲を使うのか
Section titled “なぜイベント委譲を使うのか”一覧の行は render() のたびに作り直される。
つまり、ボタンに直接イベントを付けても、再描画のたびに消えてしまう。
tbody に click を 1つ付ける ↓クリックされた要素が delete-button か確認する ↓該当 bookId を削除するこの方法なら、行が何度描き直されてもイベント設定をやり直さなくてよい。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新し、一覧が表示されることを確認する
- どれか 1 行の「削除」を押す
- 押した行だけが消えることを確認する
- 集計カードの件数も同じだけ減ることを確認する
- すべて削除すると
まだ書籍はありません。が再表示されることを確認する
ここで見たいのは、「state.books を減らした結果が、行数・件数・空メッセージへまとめて反映されるか」である。
9. ここまでの全体像
Section titled “9. ここまでの全体像”const state = { books: [...] };const elements = { ... };
function readBookFormData() { ... }function createBook(formData) { ... }function renderSummary() { ... }function renderBooks() { ... }function render() { ... }
render();
function resetForm() { ... }elements.form.addEventListener('submit', ...);elements.tbody.addEventListener('click', ...);この構成は単純だが、
- state
- render
- event
の 3 つが分かれているため、あとから入力チェックや状態更新、API 通信を追加しやすい。
よくある失敗
Section titled “よくある失敗”| 失敗 | 症状 | 直し方 |
|---|---|---|
event.preventDefault() を忘れる | 送信するとページが再読み込みされ、追加した行が見えない | submit イベントの最初で止める |
elements.price.value をそのまま使う | 2800 が文字列のまま残り、計算や比較でズレる | Number() で数値へ変換する |
renderBooks() で tbody を空にしていない | 同じ本が何度も増殖して見える | elements.tbody.innerHTML = '' で描き直す前に初期化する |
dataset.bookId の名前が HTML と JS でずれている | 削除ボタンを押しても消えない | data-book-id ↔ dataset.bookId の対応を確認する |
render() を最後に呼んでいない | state に初期データがあっても表が空のまま | 初期化時に 1 回呼ぶ |
JavaScript の DOM 操作は、Console と Elements を行き来しながら確認する と理解しやすい。
1. Console で state を見る
Section titled “1. Console で state を見る”console.table(state.books);これで、今ブラウザが持っている本の一覧を表形式で見られる。
「画面に出ない」のに state.books には入っているなら、原因は render 側にある。
2. DOM の中身を直接確認する
Section titled “2. DOM の中身を直接確認する”document.querySelectorAll('#book-table-body tr').length;document.querySelector('#empty-message').hidden;- 行数が何件あるか
- 空メッセージが隠れているか
をその場で確かめられる。
3. Elements パネルで tbody の中を見る
Section titled “3. Elements パネルで tbody の中を見る”期待する tr や button[data-book-id] が本当に出ているかを確認する。
もし DOM に行があるのに見た目が変なら CSS 側、DOM 自体がなければ JavaScript 側の問題だと切り分けられる。
4. submit 後の値の流れを追う
Section titled “4. submit 後の値の流れを追う”入力欄の値 ↓ readBookFormData()formData ↓ createBook()newBook ↓ state.books.unshift()state.books ↓ render()DOMどこでずれたか分からなくなったら、この順に console.log() を入れて確認するとよい。
| 項目 | ポイント |
|---|---|
| state | 画面の元になる正しいデータをまとめて持つ |
querySelector | HTML の目印 (id) を頼りに要素を取得する |
addEventListener | submit や click をきっかけに処理を実行する |
render() | state から件数表示と一覧テーブルを描き直す |
preventDefault() | フォーム送信時の再読み込みを止める |
| イベント委譲 | 再描画される一覧でも削除ボタンを扱いやすくする |
| デバッグ | Console で state、Elements で DOM の結果を確認する |
次のステップ
Section titled “次のステップ”演習問題 で、state・render・イベントの流れを自分の言葉と手で確認しよう。
理解できたら 10-3. 更新・削除・入力チェックを追加する へ進み、続けて使っても壊れにくい画面へ育てよう。