12.5 検索機能の実装と全体の結合

いよいよ、この章も大詰めです。
最後に、検索キーワードに合わせて結果を絞り込む機能を追加し、HTML が最初に読み込まれた時の処理を変更します。

book-search.js のソースコードを、以下の様に実装してください。これで全ての機能が完成します。

ソースフォルダ:public/ch12

ファイル名:book-search.js

// book-search.js

// すべての処理を統合する、起動時のメイン処理(最上部のコードを以下のように完成させます)
document.addEventListener("DOMContentLoaded", async () => {
	// 1. 通信を開始してデータを取得
	const data = await fetchBookData();

	// 2. クラスをインスタンス化してデータを管理
	const manager = new BookManager();
	manager.setBooks(data.slice(0, 15)); // 練習用に最初の15件を使用します

	// 3. 初期状態のリストを表示
	renderList(manager.getProcessedItems());

	// 4. 検索機能を有効化
	setupSearch(manager);

});

// 書籍データを取得する関数(枠組み)
async function fetchBookData() {
	try {
		// 1. fetchを使って外部サーバーへデータのリクエストを送信します
		const response = await fetch("https://jsonplaceholder.typicode.com/posts");
		
		// 2. 通信が成功したか(ステータスコードが200系か)をチェックします
		if (!response.ok) {
			throw new Error(`HTTPエラーが発生しました。ステータス: ${response.status}`);
		}
		
		// 3. 届いたデータをJavaScriptで扱えるオブジェクト形式(JSON)に変換します
		const books = await response.json();
		
		// 通信が成功したことを確認するためのメッセージ
		console.log(`通信成功!${books.length}件のデータを取得しました。`);
		
		return books;
	} catch (error) {
         // 通信に失敗した場合のエラーメッセージ
		console.log(`データの取得に失敗しました: ${error.message}`);
		return [];
	}
}

// 書籍データを効率的に管理するためのクラス
class BookManager {
    constructor() {
        this.books = []; // データを保持するプロパティ
    }

    // 取得したデータをクラス内にセットします
    setBooks(data) {
        this.books = data;
    }

    // 画面表示に不要な情報を削り、整形されたデータを返します
    getProcessedItems() {
        // mapを使って、idとtitleだけの新しいオブジェクト配列を作成します
        return this.books.map(book => ({
            id: book.id,
            title: book.title
        }));
    }
}

// 画面のリスト(ul要素)を更新する関数
function renderList(items) {
    const listElement = document.querySelector("#book-list");
    listElement.innerHTML = ""; // 一度中身を空にして初期化します

    // 1件ずつHTML要素(liタグ)を作成して追加します
    items.forEach(item => {
        const li = document.createElement("li"); // <li>を作成
        li.textContent = `[ID:${item.id}] ${item.title}`; // テキストを設定
        listElement.appendChild(li); // ulの中に追加
    });
}

function setupSearch(manager) {
	const searchBox = document.querySelector("#search-box");
	searchBox.addEventListener("input", (event) => {
		const keyword = event.target.value;
		// キーワードを含むものだけを抽出
		const filtered = manager.books.filter(book => 
			book.title.includes(keyword)
		);
		renderList(filtered);
	});
}

実行結果

解説

処理全体の流れ

【ステップ 1DOMContentLoaded イベント】
HTML の解析が完了したタイミングで実行されます。第 8 章で学んだ内容ですね。
async をつけている理由は、関数内で await を使うため(fetchBookData()の完了を待つ)です。

3: document.addEventListener("DOMContentLoaded", async () => {

【ステップ 2 データ取得】
await をつけることで、データ取得が完了するまで次の処理(8 行目以降)に進みません。
もし await がないと、データ取得中なのに先に進んでしまい、data が undefined の状態でエラーになってしまいます。

6: const data = await fetchBookData();

【ステップ 3-4】クラスへのデータ格納
slice(0, 15)で最初の 15 件だけを取り出しています。これは練習用のため、データ量を減らしているだけです。実際のアプリでは、manager.setBooks(data); として全件を使用します。

9:   const manager = new BookManager();
10: manager.setBooks(data.slice(0, 15));

【ステップ 5-6】画面への表示
getProcessedItems()で加工されたデータを取得し、それを renderList()に渡して画面に表示します。

13: renderList(manager.getProcessedItems());

【ステップ 7】検索機能の有効化
検索ボックスでのイベント監視を開始します。manager オブジェクトを渡すことで、setupSearch 内で元のデータにアクセスできるようになります。

16: setupSearch(manager);

event.target.value の取得

82 行目で入力されたテキストを取得しています。

82: const keyword = event.target.value;

【event オブジェクト】
イベントが発生したときの詳細情報を持つオブジェクトです。
addEventListener の引数として自動的に渡されます。

【event.target】
イベントが発生した要素(今回は検索ボックス)を指します。

【event.target.value】
その要素の現在の値(入力されているテキスト)を取得します。

filter + includes の組み合わせ

84-86 行目で、入力されたキーワードを含む書籍だけを抽出しています。

84: const filtered = manager.books.filter(book =>
85:     book.title.includes(keyword)
86: );

【filter メソッド(第 5 章の復習)】
配列の各要素をテストして、条件に合うものだけを新しい配列として返します。

【includes メソッド】
文字列に指定した文字列が含まれているか(部分一致)を判定します。

includes(“test”)
“this is a test” → true(含まれている)
“hello world” → false(含まれていない)

【大文字小文字の区別】
includes()は大文字小文字を区別し、”java” と “Java” は別物として扱われます。
大文字小文字を区別しない場合は、以下のように小文字に統一してから判定すると良いでしょう。
book.title.toLowerCase().includes(keyword.toLowerCase())

リアルタイム絞り込みの仕組み

87 行目で、絞り込み結果を画面に反映しています。

87: renderList(filtered);

【動作の流れ】

  1. ユーザーが 1 文字入力
  2. input イベント発火
  3. filter()で絞り込み
  4. renderList()で再描画
  5. 画面が更新される

この 1-5 が、文字を入力するたびに瞬時に実行されます。