🦓

[Rails]タブメニュー

2023/08/06に公開

はじめに

tailwindcss-stimulus-componentsを使ってタブメニューを実装していきます。
https://github.com/excid3/tailwindcss-stimulus-components

まだインストールしてない場合こちらからご参考にしてみてください。

tailwindcss-stimulus-components

環境

Rails 7.0.4.3
ruby 3.2.1

タブモジュールを読み込む

app/javascript/controllers/index.js
import { Tabs } from "tailwindcss-stimulus-components"
application.register('tabs', Tabs)

stimulusコントローラーを作成する

bin/rails g stimulus tabs
      create  app/javascript/controllers/tabs_controller.js

データ属性を設定する

ソースコードを読みながら設定していきます。

app/views/profiles/show.html.erb
<div data-controller="tabs" data-tabs-active-tab="-mb-px border-l border-t border-r rounded-t">
 # タブメニュー
  <ul class="list-reset flex border-b">
    <li class="-mb-px mr-1" data-tabs-target="tab" data-action="click->tabs#change">
      <a class="bg-white inline-block py-2 px-4 text-blue-500 hover:text-blue-700 font-semibold no-underline" href="#">Active</a>
    </li>
    <li class="mr-1" data-tabs-target="tab" data-action="click->tabs#change">
      <a class="bg-white inline-block py-2 px-4 text-blue-500 hover:text-blue-700 font-semibold no-underline" href="#">タブ1</a>
    </li>
    <li class="mr-1" data-tabs-target="tab" data-action="click->tabs#change">
      <a class="bg-white inline-block py-2 px-4 text-blue-500 hover:text-blue-700 font-semibold no-underline" href="#">タブ2</a>
    </li>
    <li class="mr-1">
      <a class="bg-white inline-block py-2 px-4 text-gray-300 font-semibold no-underline" href="#">タブ3</a>
    </li>
  </ul>

 # タブコンテンツ
  <div class="py-4 px-4 border-l border-b border-r" data-tabs-target="panel">
    コンテンツ1
  </div>

  <div class="py-4 px-4 border-l border-b border-r" data-tabs-target="panel">
    コンテンツ2
  </div>

  <div class="py-4 px-4 border-l border-b border-r" data-tabs-target="panel">
    <iframe width="560" height="315" src="https://www.youtube.com/embed/y3niFzo5VLI" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
  </div>
</div>

data-controller="tabs:コントローラーを指定します。
data-tabs-active-tab="-mb-px border-l border-t border-r rounded-t":アクティブのタブに追加されるクラス名です。
data-tabs-inactive-tab="inactive":インアクティブのタブに追加されるクラス名です。
data-tabs-target="tab":タブとなる要素に追加します。
data-action="click->tabs#change":タブをクリックされたらtabsコントローラーのchangeアクションと紐付けます。
data-tabs-target="panel":アクションのターゲットとなる要素を指定します。タブのコンテンツの部分になります。

https://github.com/excid3/tailwindcss-stimulus-components/blob/master/docs/tabs.md

tabs_controller.jsを設定する

JSのソースコードも見ていきます。

  static classes = [ "activeTab", "inactiveTab" ]
  • 静的クラスプロパティとして、コントローラーが使用するCSSクラスのリストを定義しています。
  • activeTabinactiveTabという2つのクラスを利用します。
  static targets = ['tab', 'panel', 'select']
  • コントローラーが操作するHTML要素のリストを定義しています。
  • tabpanelselectという名前の要素を操作するので指定します。
  static values = {
    index: 0,
    updateAnchor: Boolean
  }
  • indexは現在のタブのインデックスとなります。初期値は0です。
  • updateAnchorBoolean値を持ち、URLのアンカーを更新するかどうかを管理します。
  connect() {
    if (this.anchor) this.indexValue = this.tabTargets.findIndex((tab) => tab.id === this.anchor)
    this.showTab()
  }
  • connect()メソッドは、コントローラーがDOMに接続されると呼び出されるメソッドです。this.anchorが存在する場合、tabTargets内の要素の中で、this.anchorと一致するIDを持つ要素のインデックスをindexValueに設定します。その後、showTab()メソッドを呼び出して、現在のタブを表示します。
  change(event) {
    if (event.currentTarget.tagName === "SELECT") {
      this.indexValue = event.currentTarget.selectedIndex
    } else if (event.currentTarget.dataset.index) {
      this.indexValue = event.currentTarget.dataset.index
    } else if (event.currentTarget.dataset.id) {
      this.indexValue = this.tabTargets.findIndex((tab) => tab.id == event.currentTarget.dataset.id)
    } else {
      this.indexValue = this.tabTargets.indexOf(event.currentTarget)
    }
    window.dispatchEvent(new CustomEvent('tsc:tab-change'))
  }
  • change(event)メソッドは、タブがクリックされたときに呼び出されるメソッドです。クリックされた要素の種類に応じて、indexValueを更新します。
  • "SELECT"要素の場合は、選択されたオプションのインデックスをindexValueに設定し、data-index属性がある場合はその値を使います。
  • data-id属性がある場合は、tabTargets内の要素の中で、data-idと一致するIDを持つ要素のインデックスをindexValueに設定します。
  • それ以外の場合は、クリックされた要素自体のインデックスをindexValueに設定します。
  • 最後に、tsc:tab-changeという名前のカスタムイベントをwindowオブジェクトにdispatchします。
プールダウンセレクト
<div data-controller="tab">
  <select data-tabs-target="tab" data-action="change->tab#change">
    <option value="tab1">Tab 1</option>
    <option value="tab2">Tab 2</option>
  </select>
  <div id="tab1" data-tabs-target="panel">Tab 1 content</div>
  <div id="tab2" data-tabs-target="panel">Tab 2 content</div>
</div>
data-index属性
<div data-controller="tab">
  <div data-tabs-target="tab" data-action="click->tab#change" data-index="0">Tab 1</div>
  <div data-tabs-target="tab" data-action="click->tab#change" data-index="1">Tab 2</div>
  <div data-tabs-target="panel">Tab 1 content</div>
  <div data-tabs-target="panel">Tab 2 content</div>
</div>
data-id属性
<div data-controller="tab">
  <div data-tabs-target="tab" data-action="click->tab#change" data-id="tab1">Tab 1</div>
  <div data-tabs-target="tab" data-action="click->tab#change" data-id="tab2">Tab 2</div>
  <div id="tab1" data-tabs-target="panel">Tab 1 content</div>
  <div id="tab2" data-tabs-target="panel">Tab 2 content</div>
</div>
  nextTab() {
    this.indexValue = Math.min(this.indexValue + 1, this.tabsCount - 1)
  }

  previousTab() {
    this.indexValue = Math.max(this.indexValue - 1, 0)
  }

  firstTab() {
    this.indexValue = 0
  }

  lastTab() {
    this.indexValue = this.tabsCount - 1
  }
  • タブを切り替えるためのメソッドです。nextTab()は次のタブに、previousTab()は前のタブに、firstTab()は最初のタブに、lastTab()は最後のタブに切り替えます。
  indexValueChanged() {
    this.showTab()
    if (this.updateAnchorValue) {
      location.hash = this.tabTargets[this.indexValue].id
    }
  }
  • indexValueが変更されたときに呼び出されるメソッドです。
  • showTab()を呼び出してタブを表示し、updateAnchorValuetrueの場合、URLのアンカーを現在のタブのIDに更新します。
  showTab() {
    this.panelTargets.forEach((panel, index) => {
      const tab = this.tabTargets[index]
      console.log(tab)

      if (index === this.indexValue) {
        panel.classList.remove('hidden')
        if (this.hasInactiveTabClass) tab?.classList?.remove(...this.inactiveTabClasses)
        if (this.hasActiveTabClass) tab?.classList?.add(...this.activeTabClasses)
      } else {
        panel.classList.add('hidden')
        if (this.hasActiveTabClass) tab?.classList?.remove(...this.activeTabClasses)
        if (this.hasInactiveTabClass) tab?.classList?.add(...this.inactiveTabClasses)
      }
    })

    if (this.hasSelectTarget) {
      this.selectTarget.selectedIndex = this.indexValue
    }
  }
  • タブとパネルの表示を切り替えるためのメソッドです。panelTargetstabTargetsはそれぞれ静的ターゲットプロパティで指定したHTML要素のリストです。
  • this.indexValueindexを比較して、現在のパネルを表示または非表示にします。
  • タブに設定されたクラスを切り替えることで、アクティブなタブと非アクティブなタブを視覚的に区別します。

Image from Gyazo

  • hasSelectTargettrueの場合、selectTargetselectedIndexindexValueに設定します。

Image from Gyazo

  get tabsCount() {
    return this.tabTargets.length
  }
  • タブの数を返すゲッターメソッドです。tabTargetsの配列の長さを返します。
  get anchor() {
    return (document.URL.split('#').length > 1) ? document.URL.split('#')[1] : null;
  }
  • 現在のURLのアンカーを取得するゲッターメソッドです。document.URLを使って現在のURLを取得し、'#'で分割した配列の2番目の要素(アンカー)を返します。アンカーが存在しない場合はnullを返します。

https://github.com/excid3/tailwindcss-stimulus-components/blob/master/src/tabs.js

終わりに

tailwindcss-stimulus-componentsを使ったタブメニューでした。
デベロッパーツールを開いてブラウザの動きを確かめると分かりやすいですね。

Discussion