JavaScript | 1 日 120 分 × 7 日アプリ学習:検索 & ソート機能付き一覧

APP JavaScript
スポンサーリンク

5日目のゴールと今日やること

5日目のテーマは
「検索 × ソート × フィルタ機能を“保存できる・再利用できる・管理しやすい”形に進化させる」
ことです。

ここまでであなたは、

  • リアルタイム検索
  • 昇順 / 降順ソート
  • 複合条件フィルタ
  • タグフィルタ(AND / OR)
  • ページネーション
  • デバウンス
  • ハイライト表示

といった、実務レベルの一覧アプリの基礎を作り上げました。

5日目では、さらに一歩進んで

  • 検索条件の保存(localStorage)
  • 条件プリセット(ワンクリックで条件セット)
  • データの追加・削除
  • ソート UI の改善(状態に応じてアイコン変更)
  • applyFilters の最適化

など、「毎日使える一覧アプリ」 に仕上げるための機能を実装します。


検索条件を保存する(localStorage)

なぜ保存が必要なのか

ユーザーは毎回同じ条件で検索することが多いです。

  • 「30歳以上」
  • 「frontend タグ」
  • 「名前順の昇順」

これらを毎回設定し直すのは面倒です。

そこで、検索条件(filters / sortState / pagination)を
localStorage に保存します。


保存するデータの構造を決める

保存するオブジェクト

const savedState = {
  filters,
  sortState,
  pagination
};
JavaScript

保存関数

function saveState() {
  try {
    const json = JSON.stringify(savedState);
    localStorage.setItem("listAppState", json);
  } catch (error) {
    console.error("状態の保存に失敗しました", error);
  }
}
JavaScript

読み込み関数

function loadState() {
  const saved = localStorage.getItem("listAppState");
  if (!saved) return;

  try {
    const parsed = JSON.parse(saved);

    Object.assign(filters, parsed.filters);
    Object.assign(sortState, parsed.sortState);
    Object.assign(pagination, parsed.pagination);

    applyFilters();
  } catch (error) {
    console.error("状態の読み込みに失敗しました", error);
  }
}
JavaScript

深掘りポイント

  • JSON.stringify / JSON.parse は localStorage の基本
  • 保存するのは「UI の状態」
  • 読み込んだら applyFilters を呼んで即反映

これで「前回の続きから使えるアプリ」になります。


条件プリセットを作る(ワンクリックで条件セット)

UI のイメージ

<button class="preset" data-keyword="a" data-min="20" data-max="30">20〜30歳 / 名前に a</button>
<button class="preset" data-tag="frontend">Frontend のみ</button>
<button class="preset" data-sort="age-asc">年齢昇順</button>

プリセットのロジック

const presetButtons = document.querySelectorAll(".preset");

presetButtons.forEach(btn => {
  btn.addEventListener("click", () => {
    const keyword = btn.dataset.keyword || "";
    const minAge = btn.dataset.min ? Number(btn.dataset.min) : null;
    const maxAge = btn.dataset.max ? Number(btn.dataset.max) : null;
    const tag = btn.dataset.tag || null;
    const sort = btn.dataset.sort || null;

    if (keyword !== undefined) filters.keyword = keyword;
    if (minAge !== null) filters.minAge = minAge;
    if (maxAge !== null) filters.maxAge = maxAge;

    if (tag) filters.tags = [tag];

    if (sort) {
      const [key, order] = sort.split("-");
      sortState.key = key;
      sortState.order = order;
    }

    applyFilters();
  });
});
JavaScript

深掘りポイント

  • data 属性で条件を柔軟に設定
  • ボタンを押すだけで複数条件を一気にセット
  • applyFilters で即反映

これが「実務でよく使うプリセット機能」です。


データの追加・削除を実装する

なぜ必要なのか

一覧アプリは「見るだけ」で終わらず、
データを追加・削除できる と一気に実用的になります。


データ追加フォームを作る

UI のイメージ

<input id="newName" placeholder="名前" />
<input id="newAge" type="number" placeholder="年齢" />
<button id="addUser">追加</button>

追加ロジック

addUser.addEventListener("click", () => {
  const name = newName.value.trim();
  const age = Number(newAge.value);

  if (!name || !Number.isFinite(age)) {
    alert("正しい名前と年齢を入力してください");
    return;
  }

  const newUser = {
    id: Date.now(),
    name,
    age,
    tags: []
  };

  users.push(newUser);
  applyFilters();
});
JavaScript

深掘りポイント

  • id は Date.now() で簡易生成
  • users.push で追加
  • applyFilters で即反映

データ削除機能を追加する

renderList に削除ボタンを追加

function renderList(arr) {
  if (!arr.length) {
    listDiv.textContent = "該当するユーザーはいません。";
    return;
  }

  let html = "";
  arr.forEach(user => {
    const name = highlight(user.name, filters.keyword);

    html += `
      <p>
        ${name}${user.age}歳)
        <button class="delete" data-id="${user.id}">削除</button>
      </p>
    `;
  });

  listDiv.innerHTML = html;

  const deleteButtons = document.querySelectorAll(".delete");
  deleteButtons.forEach(btn => {
    btn.addEventListener("click", () => {
      const id = Number(btn.dataset.id);
      deleteUser(id);
    });
  });
}
JavaScript

削除ロジック

function deleteUser(id) {
  const index = users.findIndex(u => u.id === id);
  if (index !== -1) {
    users.splice(index, 1);
    applyFilters();
  }
}
JavaScript

深掘りポイント

  • findIndex で対象を探す
  • splice で削除(破壊的だが users は「データベース」扱いなので OK)
  • applyFilters で即反映

ソート UI を改善する(アイコン切り替え)

ボタンの表示を動的に変更

function updateSortButtons() {
  sortAge.textContent =
    sortState.key === "age"
      ? `年齢 ${sortState.order === "asc" ? "▲" : "▼"}`
      : "年齢 ▲▼";

  sortName.textContent =
    sortState.key === "name"
      ? `名前 ${sortState.order === "asc" ? "▲" : "▼"}`
      : "名前 ▲▼";
}
JavaScript

applyFilters の最後に追加します。

updateSortButtons();
JavaScript

深掘りポイント

  • 現在のソート状態を UI に反映
  • ユーザーが「今どうなっているか」を理解しやすくなる

applyFilters を最適化する

3日目までの applyFilters はこうでした

  • キーワード
  • 年齢
  • タグ
  • ソート
  • ページネーション

これを「小さな関数の組み合わせ」にして読みやすくします。


applyFilters の完成形

function applyFilters() {
  let result = [...users];

  result = applyKeywordFilter(result);
  result = applyAgeFilter(result);
  result = applyTagFilter(result);
  result = applySort(result);
  result = applyPagination(result);

  renderList(result);
  updateSortButtons();
  saveState();
}
JavaScript

深掘りポイント

  • 小さな関数の組み合わせで読みやすい
  • UI 更新(renderList / updateSortButtons)
  • 状態保存(saveState)

これが「中級者の一覧アプリの型」です。


5日目のまとめ

今日あなたがやったことを整理すると、こうなります。

  • 検索条件を localStorage に保存
  • 条件プリセットを実装
  • データ追加・削除機能を追加
  • ソート UI を改善(アイコン切り替え)
  • applyFilters を小さな関数の組み合わせに最適化
  • UI とロジックの分離をさらに強化

どれも実務で必須のテクニックです。


今日いちばん深く理解してほしいこと

5日目の本質は、

「一覧アプリは、状態(state)を保存し、再利用し、UI とロジックを分離することで“毎日使えるツール”になる」

ということです。

検索
→ フィルタ
→ ソート
→ ページネーション
→ 保存
→ 再利用

この流れを理解できたあなたは、
もう「ただ動くアプリ」ではなく
“使い続けられるアプリ” を作れる段階に来ています。

6日目では、この一覧アプリに

  • データ編集(インライン編集)
  • 並び替え(ドラッグ & ドロップ)
  • UI の最適化
  • 状態管理の高度化

などを加えて、さらに完成度を高めていきます。

タイトルとURLをコピーしました