Tab(タブ)

Tab(タブ)

実装例

今回も jQuery を使いつつ、Tab(タブ)を実装してみました。先程のモーダルと比べると複雑に見えますが、ひとつずつ見ていきましょう。

HTML

<ul class="tabs">
<li class="tab is-active" data-tab="tab1">Tab1</li>
<li class="tab" data-tab="tab2">Tab2</li>
<li class="tab" data-tab="tab3">Tab3</li>
</ul>
<section class="tab-contents">
<div id="tab1" class="tab-content is-active">hoge</div>
<div id="tab2" class="tab-content">fuga</div>
<div id="tab3" class="tab-content">piyo</div>
</section>

HTMLは大きく二つに分かれています。ページ上部のタブ(class="tab")ラベルの部分の要素と、タブの中身(class="tab-content")です。どちらも現在有効なもの(選択・表示中とするもの)には is-active が付くようになっています。

この実装ではタブの中身の要素をHTMLに直で書いているので、後述のJSでDOMを動的に生成する方式と比べて、読みやすい実装が可能です。

JS

// タブ要素の取得(document.querySelectorAllと同義)
const $tabs = $(".tab");
// タブコンテンツの取得
const $contents = $(".tab-content");
// ".tab"の各要素にクリックイベントハンドラを定義する。
$tabs.on("click", function () {
// クリックされた要素(= 単一の ".tab" 要素)を取得する。
const $tab = $(this);
// タブ要素の "data-tab" 属性の値を取得する。
const tabId = $tab.data("tab");
// タブの制御
// まずすべてのタブの "is-active" を消す。
$tabs.removeClass("is-active");
// その後、自身(タブ)に "is-active" を足す。
$tab.addClass("is-active");
// タブのコンテンツの制御
// まずすべてのコンテンツの "is-active" を消す。
$contents.removeClass("is-active");
// 選択されたタブと関連するコンテンツに "is-active" を足す。
$(`#${tabId}`).addClass("is-active");
});

上から順に読んでいきましょう。

  1. まず タブの一覧・タブのコンテンツの一覧 を取得しています。

  2. $tabs.on("click", function...) の部分では、タブの各要素(例では3つの ".tab" を持つ要素)に対して一括でクリックイベントのハンドラを定義しています。ブラウザの標準APIではel.addEventListener("click", function...)と同じ意味ですが、複数の要素に一括で指定できる。という差があります。

  3. タブのイベントハンドラの中ではクリックされると以下の操作を行います。これをCSSと組み合わせる事で、タブをクリックすると選択状態が切り替わる + タブのコンテンツが切り替わる。という動きを実現しています。

    1. クリックされたタブの関連情報を取得($(this), $tab.data("tab"))

    2. タブの選択状態をクリア(removeClassでクラスを削除)し、クリックされたタブを選択状態("is-active" をクラスに追加)とする。

    3. タブのコンテンツをすべて非表示(removeClassでクラスを削除)とし、クリックされたタブのコンテンツのみを表示状態("is-active" をクラスに追加)とする。

前回と同様にJSからはクラス名の操作のみを行い、実際のタブの表示(選択状態・表示/非表示)の切り替えはCSSで行っています。このルールを守る事を心がけておけば、後から読み返しても分かりやすいコードになるので覚えておきましょう。

CSS

.tabs {
/* ブラウザのulの標準スタイルをリセットする */
margin: 0;
padding: 0;
display: flex;
list-style: none;
}
.tabs .tab {
padding: 4px 8px;
border-left: 1px solid #CCCCCC;
border-top: 1px solid #CCCCCC;
font-weight: bold;
cursor: pointer;
}
.tabs .tab.is-active {
background-color: #CCCCCC;
}
.tabs .tab:last-of-type {
border-right: 1px solid #CCCCCC;
}
.tab-content {
padding: 8px;
border: 1px solid #CCCCCC;
display: none;
}
.tab-content.is-active {
display: block;
}

CSSでは要素の状態の切り替えを "is-active" クラスが付いているかどうか。で切り替えています。これは SMACSS というCSSのスタイルガイドラインに準拠した考え方(状態(ステート)ルール と呼ばれているもの)です。基本的な読み方をおさらいしておきます。

  • .tabs .tab と書くと、 .tabs の配下にある .tab 要素のセレクタを表します。

  • .tab.is-active と書くと、 .is-active クラスの付いた、 .tab 要素(= 両方のクラスを持った)のセレクタを表します。

  • .tab:last-of-type と書くと、 .tab クラスを持つ最後の要素のセレクタを表します。

個別のプロパティ(例: display) についても説明しておきます。

  • list-syle: none = ulタグのデフォルトのリスト表示( = 行頭の中黒記号)を無効(非表示)にする。

  • display = 要素の表示の切り替え。none = 非表示、block = ブロック表示(ここでは単なる 表示(見えるようにする) )という意味となる。

ここまででタブの実装は完了なのですが、このやり方とは別のやり方(JSでDOMを動的に生成するやり方)を紹介しておきます。

Tab(タブ) - JS動的生成版

前回のものと見た目は同一ですが、コードが変わっています。それぞれ見ていきます。

HTML

<div id="tabs"></div>

前のものと比べるとかなりアッサリしました。これはJSで要素を描画する対象をid="tabs" として定義しているだけです。前回のサンプルに含まれていたHTMLタグはJSのコード側で生成することになります。

JS

// 描画対象のタブの一覧の定義
const tabs = [
{ id: "tab1", label: "Tab1", content: "hoge" },
{ id: "tab2", label: "Tab2", content: "fuga" },
{ id: "tab3", label: "Tab3", content: "piyo" }
];
// currentTabId = 選択状態とするタブのID
// currentTabIdを引数として、タブ要素を描画する関数
const render = (tabs, currentTabId) => {
// 現在のタブを取得する。
const currentTab = tabs.find((tab) => tab.id === currentTabId);
// タブの単一行のHTMLを生成する関数
const getTabHtml = (tab) => {
let tabClass = "tab";
if (tab.id === currentTabId) {
tabClass += " is-active";
}
return `<li class="${tabClass}" data-tab="${tab.id}">${tab.label}</li>`;
};
// tab一覧のHTMLを生成
// tabs.map(...)の部分ではtabsの各行に対して "getTabHtml" を実行することで、全行分のHTMLを生成している。
const tabsHtml = `
<ul class="tabs">
${tabs.map(getTabHtml).join("")}
</ul>
`;
// コンテンツのHTMLを生成
const contentHtml = `
<section class="tab-contents">
<div id="${currentTabId}" class="tab-content is-active">${currentTab.content}</div>
</section>
`;
// id="tabs"の要素のHTMLを書き換える = タブ要素の描画。
$("#tabs").html(`
${tabsHtml}
${contentHtml}
`);
// タブのクリック時に、クリックされたタブを選択状態として、再描画を行う。
$(".tab").on("click", function () {
render(tabs, $(this).data("tab"));
});
};
// 初回描画
render(tabs, "tab1");

HTMLが減ったのに対して、JSのコードはちょっと増えています。上から説明してきます。

  1. const tabs = [{...}, {...}, {...}] の部分でデータ(タブ・タブ要素の一覧を定義)を定義。

  2. render("tab1") で "tab1" を初期状態としてHTMLの描画関数を実行。

  3. renderの中でまず選択中のタブ(currentTabの部分)を取得

  4. tabsHtml, contentHtml, getTabHtml() の中でHTMLタグの文字列を組み立て。 = 前回のサンプルのHTMLと同等のタグを文字列として組み立てる。

  5. $('#tabs').html(`...`) の部分で、生成したHTMLをDOM(ページ)に反映

  6. $(".tab").on("click", function...) の部分で、タブがクリックされた時に、そのタブを選択状態として再描画するようにイベントハンドラを定義する。

項番4 のHTMLタグを文字列として組み立てる所が複雑なのですが、こちらのコードの方が前回のサンプルよりも以下の点で優れています。

  • タブを増やす時に、const tabs = [{...}, {...}, {...}]の部分に1つ増やすだけで、画面に反映される。 = 管理が楽。たとえばAJAX通信でサーバ側の設定値を元にタブを増やしたりできる。

  • 関数として描画処理がまとまっているため、再利用(他の箇所に転用)しやすい。

ですが、メリットばかりではなく、デメリットもあります。

  • 前回のサンプルよりもコードが複雑なので、読みづらい。= 後から処理を書き換えたり追加しづらい。

  • ページのHTMLを書き換えるという仕組み上、XSS に注意が必要。例えばAPIサーバからtabsの設定値を取得する際は、不適切なHTML文字列が含まれないように適切にエスケープ(= 無害化)する必要がある。(例: HTMLタグとして利用される変数(例: { id: "", label: "", content: "xxx" })に <script>alert("hoge")</script> という文字列が含まれていると、alertが表示される = 任意のスクリプトが実行される)

まとめ

このように、同じ タブ要素を作る という課題に対して、複数の解決のアプローチがあります。それぞれのメリット・デメリットも正しく理解したうえで、適切な判断を出来るようにしましょう。