コード例
作りたいの下記のUIです。 デモはこちらにあります。
MPAの場合は、現在のURLからサイドバーの状態を推測し、それを反映したHTMLをブラウザに送ります。通常はこれで問題がありませんし、UI/UXとして十分です。
しかし今回は別のアプローチを取ります。サイドバーの状態をブラウザのステートと考え、初回ロード以降はサーバからは変更しないやり方です。
<turbo-frame>
タグやTurbo Stream用のタグをつける必要があり、煩雑ですaria-expanded
を使えばアクセシビリティと一石二鳥になりますので、HTMLaria-expanded
属性でステートを持つようにしますさらに「"Teams"のサブメニューは開いたまま」という要件を満たすために次のことも行います。
data-turbo-permanent
を使用しますaria-current
というものが今回の用途にぴったりですので、ステートはCSSではなくaria属性でStimulusのステートを持たせます(この方がデザイン変更に強くなります)要件は多いのですが、コードは比較的シンプルです。
<div class="flex"> <%= render 'sidebar' %> <div class="flex-grow"> <%= image_tag "component_images/demo-dashboard.webp", class: "w-full" %> </div> </div>
<div class="flex"> <%= render 'sidebar' %> <div class="flex-grow"> <%= image_tag "component_images/demo-engineering-team.webp", class: "w-full" %> </div> </div>
sidebar
パーシャルで表示しています。全く同じものを表示していることが確認できると思います。sidebar
パーシャル<div data-turbo-permanent id="sidebar" data-controller="sidebar"> <div class="h-full w-40 flex shrink-0 flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6"> <nav class="flex flex-1 flex-col"> <ul role="list" class="flex flex-1 flex-col gap-y-7"> <li> <ul role="list" class="-mx-2 space-y-1"> <li> <!-- Current: "bg-gray-50", Default: "hover:bg-gray-50" --> <%= link_to "Dashboard", component_path(:sidebar), aria: {current: "page"}, data: { action: "click->sidebar#setCurrent" }, class: "block rounded-md aria-[current=page]:bg-gray-50 py-2 pl-10 pr-2 text-sm/6 text-gray-700" %> </li> <li> <div> <button type="button" class="group peer flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm/6 font-semibold text-gray-700 hover:bg-gray-50" data-action="click->sidebar#toggle" aria-controls="sub-menu-teams" aria-expanded="false"> <!-- Expanded: "rotate-90 text-gray-500", Collapsed: "text-gray-400" --> <svg class="size-5 shrink-0 text-gray-400 group-aria-expanded:rotate-90 group-aria-expanded:text-gray-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-slot="icon"> <path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /> </svg> Teams </button> <!-- Expandable link section, show/hide based on state. --> <ul class="mt-1 px-2 hidden peer-aria-expanded:block" id="sub-menu-teams"> <li> <%= link_to "Engineering", component_path(:sidebar_other_page), aria: {current: "false"}, data: { action: "click->sidebar#setCurrent" }, class: "block rounded-md py-2 pl-9 pr-2 text-sm/6 text-gray-700 hover:bg-gray-50 aria-[current=page]:bg-gray-50" %> </li> </ul> </div> </li> </ul> </li> </ul> </nav> </div> </div>
data-turbo-permanent id="sidebar"
を設定しています。上述した通り、これによってサイドバーのHTML要素は固定されて、Turbo Driveで新しいページを読み込んでも、新しいHTMLで上書きされません。なおid
が必須になりますdata-controller="sidebar"
があります。これでsidebar
Stimulus controllerに接続されますa
タグですので、Turbo Driveによるページ遷移をしますdata-action="click->sidebar#setCurrent"
があります。SidebarController
Stimulus controllerのsetCurrent()
メソッドが呼ばれますaria-current="page"または"false"
をつけていますbutton
タグのとこにdata-action="click->sidebar#toggle"
をつけます。SidebarController
Stimulus controllerのtoggle()
メソッドが呼び出されますaria-expanded
に持ちますので、初期状態としてaria-expanded="false"
をつけていますimport { Controller } from "@hotwired/stimulus" // Connects to data-controller="sidebar" export default class extends Controller { connect() { } toggle(event) { const button = event.currentTarget button.ariaExpanded = button.ariaExpanded === "true" ? "false" : "true" } setCurrent(event) { this.#resetAriaCurrent() const link = event.currentTarget link.ariaCurrent = "page" } #resetAriaCurrent() { this.element .querySelectorAll("[aria-current]") .forEach(e => e.ariaCurrent = "false") } }
toggle(event)
はサブメニュー開閉ボタンをトグルするものです。表示を変更するターゲットとなるHTML要素は自分自身(data-action
を持ったHTML要素自身)ですので、event.currentTarget
で取得できます。このHTML要素のaria-expanded
を適宜設定していますsetCurrent(event)
は選択されたリンクの背景を灰色にするものです。先にthis.#resetAriaCurrent()
で今まで選択されていたものをクリアしたのち、data-action
を受けたHTML要素(event.currentTarget
で取得)にaria-current="page"
を設定していますdata-turbo-permanent
で行いますa
タグをクリックしたと同時にTurbo DriveとStimulusの双方が動いています。したがって2番目の緑の経路と、3番目の青の経路を使った感じになっていますdata-turbo-permanent
で固定化する方法です。個々のケースで最良のものを選択します。