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

Web APP JavaScript
スポンサーリンク

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

4日目のテーマは
TabRouter を“アプリ全体の画面遷移の土台”として育てる」ことです。

ここまでであなたは、

URL hash を「今どの画面か」という状態として扱う
TabRouter クラスでタブ切り替え・履歴・再読み込み復元をまとめる
beforeChange / afterChange で画面遷移前後の処理を差し込む

というところまで来ています。

4日目では、これをもう一歩進めて、

タブごとに「専用の初期化処理」を持たせる
タブの中に「さらにタブ(ネスト)」があるケースを考える
「画面遷移管理」と「URL hash」の関係を、もう一段深く理解する

というところまで持っていきます。


「タブごとに専用のロジックがある」世界を想像してみる

現実のアプリで起きていること

実際のアプリを思い浮かべてください。

ホームタブは「お知らせ一覧」を読み込む。
プロフィールタブは「ユーザー情報フォーム」を表示する。
設定タブは「トグルスイッチやチェックボックス」が並ぶ。

つまり、
タブごとにやりたいことが全然違う わけです。

今の TabRouter は「表示の切り替え」はできますが、
「タブごとの専用処理」は外側でバラバラに書くしかありません。

ここを、もう少し“設計された形”にしていきます。


タブごとに「コントローラ」を持たせる発想

役割を分ける

TabRouter は「どのタブがアクティブか」を管理する。
タブごとのコントローラは「そのタブの中で何をするか」を管理する。

この分担をすると、コードが一気に整理されます。

例えば、こんなイメージです。

const controllers = {
  home: {
    onEnter() { console.log("ホームに入った"); },
    onLeave() { console.log("ホームから出た"); }
  },
  profile: {
    onEnter() { console.log("プロフィールに入った"); },
    onLeave() { console.log("プロフィールから出た"); }
  }
};
JavaScript

そして TabRouter 側で、
タブが変わるたびに onLeave / onEnter を呼ぶようにします。


TabRouter に「タブコントローラ」を組み込む

コントローラを受け取る

コンストラクタで、オプションとして受け取ります。

class TabRouter {
  constructor(navElement, viewElements, options = {}) {
    this.navElement = navElement;
    this.viewElements = Array.from(viewElements);
    this.validTabs = this.viewElements.map(v => v.dataset.view);

    this.controllers = options.controllers || {};
    this.defaultTab = options.defaultTab || this.validTabs[0];

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

    this.beforeChange = options.beforeChange || null;
    this.afterChange = options.afterChange || null;

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

    this.bindEvents();
    this.render();
    this.callControllerEnter(null, this.currentTab);
  }
}
JavaScript

ここで新しく出てきたのが controllers
最後の callControllerEnter(null, this.currentTab); です。

最初に表示されるタブに対しても、
「入ってきたよ」という通知をしてあげるイメージです。


onEnter / onLeave を呼び出す仕組み

共通の呼び出しメソッドを作る

callControllerEnter(from, to) {
  const controller = this.controllers[to];
  if (controller && typeof controller.onEnter === "function") {
    controller.onEnter(from, to);
  }
}

callControllerLeave(from, to) {
  const controller = this.controllers[from];
  if (controller && typeof controller.onLeave === "function") {
    controller.onLeave(from, to);
  }
}
JavaScript

ここでのポイントは 2 つです。

存在しないタブには何もしない(安全)。
from / to を渡しておくことで、必要なら遷移元も使える。

changeTab に組み込む

changeTab(nextTab) {
  const prevTab = this.currentTab;

  if (this.beforeChange) {
    const ok = this.beforeChange(prevTab, nextTab);
    if (!ok) {
      window.location.hash = prevTab;
      return;
    }
  }

  this.callControllerLeave(prevTab, nextTab);

  this.currentTab = nextTab;
  this.render();

  this.callControllerEnter(prevTab, nextTab);

  if (this.afterChange) {
    this.afterChange(prevTab, nextTab);
  }
}
JavaScript

これで、

タブが変わるたびに
前のタブの onLeave
次のタブの onEnter

が自動的に呼ばれるようになります。


具体例で「タブコントローラ」のイメージを掴む

ホームタブで「一度だけデータを読み込む」

例えば、ホームタブで API からデータを読み込みたいとします。

let homeLoaded = false;

const controllers = {
  home: {
    async onEnter(from, to) {
      if (homeLoaded) return;
      const list = document.querySelector("#homeList");
      list.textContent = "読み込み中…";

      const data = await fetch("/api/home").then(res => res.json());
      list.textContent = data.items.join(", ");

      homeLoaded = true;
    }
  }
};
JavaScript

TabRouter 側は、
「タブが home になったら onEnter を呼ぶ」
という約束だけ守ればいい。

ホームタブの中で何をするかは、
コントローラ側に閉じ込められます。

プロフィールタブで「フォームを初期化する」

const controllers2 = {
  profile: {
    onEnter(from, to) {
      const form = document.querySelector("#profileForm");
      form.reset();
    },
    onLeave(from, to) {
      console.log("プロフィールタブから離れました");
    }
  }
};
JavaScript

こうしておくと、
TabRouter のコードを一切触らずに、
タブごとの振る舞いを増やしていけます。


ネストしたタブ(タブの中にタブ)をどう考えるか

よくある UI の例

設定タブの中に、さらにタブがあるケースを想像してください。

設定タブ
その中に「一般」「通知」「詳細」タブ

このとき、
「外側のタブ」と「内側のタブ」は
別々の TabRouter として考えるのが自然です。

設計の考え方

外側の TabRouter は
「home / profile / settings」などの大きな画面を切り替える。

内側の TabRouter は
「settings の中だけ」でタブを切り替える。

このとき大事なのは、
「URL hash をどこまで使うか」 です。


URL hash を「階層的に使う」かどうか

シンプルなやり方:外側だけ hash を使う

まずは一番シンプルなパターンから。

外側のタブ(home / profile / settings)は hash で管理する。
内側のタブ(settings 内のタブ)は hash を使わず、
単純な TabRouter(hash 非対応版)として作る。

この場合、
URL は #settings のままですが、
設定画面の中だけでタブが切り替わります。

初心者〜中級の段階では、
このくらいの分離で十分です。

もう一歩進めるとどうなるか(考え方だけ)

もし階層的に hash を使うなら、
#settings/notification のような形にして、
TabRouter 側で分解してあげる必要があります。

例としては、

window.location.hash"#settings/notification"
replace("#", "")"settings/notification"
split("/")["settings", "notification"]

外側の TabRouter は ["settings", ...] の先頭だけを見る。
内側の TabRouter は ["...", "notification"] の後ろだけを見る。

というような設計になります。

今日はここを「考え方レベル」で触れておくくらいにしておきます。


「画面遷移管理」と「URL hash」の関係をもう一度整理する

ここまで来ると、
URL hash は「ただの文字列」ではなく、
「アプリの状態を外に見せるための窓」
だという感覚が強くなってきたはずです。

TabRouter は、

内部に currentTab という状態を持っている。
URL hash は、その状態を URL 上に表現したもの。
hash が変わったら、それに合わせて状態を変える。
状態が変わったら、それに合わせて表示を変える。

という三段階の流れをずっと守っています。

ここにさらに、

タブごとのコントローラ(onEnter / onLeave)
beforeChange / afterChange のフック
存在しないタブ名の正規化(normalizeTabName)

が乗ってくることで、
「画面遷移管理」という言葉が
かなり“実務寄りの意味”を持ってきます。


4日目のまとめ

4日目であなたが手に入れたものを、言葉で整理します。

タブごとに「専用のロジック(コントローラ)」を持たせる発想。
TabRouter は「どのタブか」を管理し、
コントローラは「そのタブの中で何をするか」を管理する、という役割分担。
タブ遷移の前後で onLeave / onEnter を呼ぶ仕組み。
クリックでも戻る / 進むでも、必ず changeTab を通す一貫した設計。
ネストしたタブを「別の TabRouter」として考える視点。
URL hash を「状態の表現」として扱う感覚の深まり。

ここまで来ると、
あなたはもう「タブ UI を作れる人」ではなく、
「画面遷移を設計できる人」 の側に足を踏み入れています。

5日目では、
この TabRouter を「アニメーション」や「ローディング表示」と組み合わせて、
さらに“SPA っぽさ”を強めていきましょう。

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