JavaScript | 1 日 120 分 × 7 日アプリ学習:SPA風タブ切り替えアプリ

Web APP JavaScript
スポンサーリンク

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

2日目のテーマは
「1日目で作ったタブ切り替えロジックを“クラス化”して、どこでも再利用できる形にする」ことです。

昨日は関数ベースで
URL の hash を状態として扱い、
その状態に応じて表示を切り替える、という流れを作りました。

今日はそれを
TabRouter のようなクラスにまとめて、

「このナビとビューを渡せば、勝手に SPA 風タブになる」

という状態を目指します。


なぜクラス化するのかを整理する

関数ベースの限界

1日目のコードは、ざっくりこんな構成でした。

  • getCurrentTabFromHash()
  • renderTab(tabName)
  • setupTabClick()
  • hashchange イベントで renderTab()

これでも動きますが、問題が出てきます。

タブが 2 セットあったらどうするか。
別ページでも同じ仕組みを使いたくなったらどうするか。
タブの名前や構造が変わったら、どこを直せばいいか分かりにくい。

ここで「クラス設計」の出番です。

クラス化の狙い

クラスにすることで、

タブ切り替えのロジックを 1 つのまとまりとして扱える。
HTML 構造ごとにインスタンスを作れる。
外から「今どのタブ?」を聞ける。

という状態にできます。


TabRouter クラスの骨組みを作る

最小構造

まずは「何を受け取るクラスか」を決めます。

  • タブのナビゲーション要素(<nav>
  • ビューのコンテナ(<section> たち)

これを前提に、骨組みを書きます。

class TabRouter {
  constructor(navElement, viewElements, options = {}) {
    this.navElement = navElement;
    this.viewElements = Array.from(viewElements);
    this.defaultTab = options.defaultTab || "home";

    this.currentTab = this.getTabFromHash() || this.defaultTab;

    this.handleTabClick = this.handleTabClick.bind(this);
    this.handleHashChange = this.handleHashChange.bind(this);

    this.bindEvents();
    this.render();
  }
}
JavaScript

ここまでで、クラスの「入口」が決まりました。

コンストラクタで
ナビとビューを受け取り、
初期状態のタブを決めて、
イベントを結びつけて、
最初の描画を行う。

この流れが見えていれば OK です。


URL hash から「状態」を取り出すメソッド

getTabFromHash をメソッド化する

1日目の関数を、そのままクラスの中に移します。

getTabFromHash() {
  const hash = window.location.hash;
  if (!hash) return null;
  return hash.replace("#", "");
}
JavaScript

ここでのポイントは、
「hash がなければ null を返す」ことです。

コンストラクタ側で

this.currentTab = this.getTabFromHash() || this.defaultTab;
JavaScript

としているので、

hash があればそれを優先。
なければ defaultTab を使う。

という柔軟な初期化ができます。


状態に応じて表示を切り替える render メソッド

render の中身

render() {
  const active = this.currentTab;

  this.viewElements.forEach(view => {
    const name = view.dataset.view;
    if (name === active) {
      view.style.display = "block";
    } else {
      view.style.display = "none";
    }
  });

  const tabs = this.navElement.querySelectorAll("[data-tab]");
  tabs.forEach(tab => {
    const name = tab.dataset.tab;
    if (name === active) {
      tab.classList.add("is-active");
    } else {
      tab.classList.remove("is-active");
    }
  });
}
JavaScript

ここで重要なのは、
「表示は this.currentTab に従うだけ」
という構造になっていることです。

render は「状態を見て UI を合わせる」だけ。
状態を変えるのは別のメソッドの役割です。

この分離が、
「状態と表示の分離」のど真ん中です。


タブクリックで状態と URL を更新する

イベントの紐付け

bindEvents() {
  const tabs = this.navElement.querySelectorAll("[data-tab]");
  tabs.forEach(tab => {
    tab.addEventListener("click", this.handleTabClick);
  });

  window.addEventListener("hashchange", this.handleHashChange);
}
JavaScript

クリック時の処理

handleTabClick(event) {
  event.preventDefault();

  const tab = event.currentTarget;
  const tabName = tab.dataset.tab;

  if (!tabName) return;

  if (this.currentTab === tabName) return;

  this.currentTab = tabName;
  window.location.hash = tabName;
  this.render();
}
JavaScript

ここでやっていることを言葉で整理すると、

通常のリンク遷移を止める。
クリックされたタブの名前を読む。
すでにそのタブなら何もしない。
状態(currentTab)を更新する。
URL の hash を更新する。
render で表示を合わせる。

という流れです。

ポイントは、
「状態を変えるのは handleTabClick、表示を変えるのは render」
と役割を分けていることです。


hashchange で「戻る / 進む」に対応する

hashchange ハンドラ

handleHashChange() {
  const tabName = this.getTabFromHash() || this.defaultTab;

  if (this.currentTab === tabName) return;

  this.currentTab = tabName;
  this.render();
}
JavaScript

ブラウザの戻る / 進むで hash が変わったとき、

今の hash からタブ名を取り出す。
状態(currentTab)を更新する。
render で表示を合わせる。

という流れになります。

ここでも、
「状態の変更 → render」
という一貫したパターンを守っています。


再読み込み復元は「初期化時の hash 参照」で完了する

コンストラクタでこう書いていました。

this.currentTab = this.getTabFromHash() || this.defaultTab;
this.render();
JavaScript

これだけで、

URL に #settings が付いていれば settings タブから始まる。
何も付いていなければ defaultTab(例: “home”)から始まる。

つまり、

タブ切り替え → hash 更新 → 再読み込み → hash から復元

という流れが自然に成立します。

「復元のための特別な処理」は書いていません。
最初から「状態は hash から決める」と決めているから、勝手に復元される
というのが大事な感覚です。


実際の使い方のイメージ

HTML 側

<nav class="tabs" id="mainTabs">
  <a href="#home" data-tab="home">ホーム</a>
  <a href="#profile" data-tab="profile">プロフィール</a>
  <a href="#settings" data-tab="settings">設定</a>
</nav>

<section class="view" data-view="home">ホーム画面</section>
<section class="view" data-view="profile">プロフィール画面</section>
<section class="view" data-view="settings">設定画面</section>

初期化コード

document.addEventListener("DOMContentLoaded", () => {
  const nav = document.getElementById("mainTabs");
  const views = document.querySelectorAll(".view");

  const router = new TabRouter(nav, views, {
    defaultTab: "home"
  });

  // 必要なら外から状態を読むこともできる
  console.log(router.currentTab);
});
JavaScript

これで、

タブクリックで切り替わる。
戻る / 進むでタブが戻る。
再読み込みしても同じタブから始まる。

という「SPA 風タブ切り替え」が、
1 つのクラスとして再利用可能な形になります。


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

2日目の本質は、

画面遷移のロジックをクラスに閉じ込めて、
状態 → 表示 の流れを一貫したパターンにする

ということです。

URL hash は「今どの画面か」という状態の一部。
currentTab は「アプリ側が持つ状態」。
render は「状態に従って UI を合わせるだけ」。

この三角形が頭に入っていれば、
タブだけでなく、
SPA 全体の画面遷移も同じ考え方で組み立てられるようになります。

3日目では、この TabRouter を
「タブごとに初期化処理を差し込める」
「タブ遷移前後にフックを入れられる」
といった方向に発展させていきます。

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