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 を
「タブごとに初期化処理を差し込める」
「タブ遷移前後にフックを入れられる」
といった方向に発展させていきます。


