コード例
作りたいの下記のUIです。 デモはこちらにあります。

aタグで十分です。これで自動的にTurbo Driveで画面遷移します。そしてここではStimulusを使う必要はありません(別のところでは使います)aria-expandedを使えばアクセシビリティと一石二鳥になりますので、HTMLaria-expanded属性でステートを持つようにします一般的なRailsの開発であればこれでうまくいきます。
ただしそのためには遷移先のページ(今回は"Engineering"のページ)でもサイドバーメニューが表示され、かつ"Teams"サブメニューが展開されて状態になっていること、さらに"Engineering"のリンクが選択状態になっていること(背景が灰色になっていること)が必要です。今までのMPAであればこれはERBで処理しますが、意外と大変でメンテナンス上心配です。非常に多くのページのあるサイトであれば、多少UI/UXを犠牲にしたとしてもサイドバーのことはあまり考えたくないはずです。
これを解決する方法としてサイドバーのステートを維持することが考えられます。"Dashboard"ページから"Engineering"のページに遷移する際、サイドバーを再レンダリングせず、元の状態("Teams"サブメニューが開いた状態)を維持してくれれば良いのです。
そうすれば"Engineering"ページのERBで、サイドバーの開閉状態ロジックを用意しなくてもよくなります。ただし現在選択されたリンクを示す灰色のバックグラウンドだけは対応しなければなりません。
data-turbo-permanentを使用しますaria-currentというものが今回の用途にぴったりですので、ステートはCSSではなくaria属性でStimulusのステートを持たせます(この方がデザイン変更に強くなります)説明は長くなりましたが、コードは比較的シンプルです。以下に解説します。
<div class="flex"> <%= render 'sidebar' %> <div class="flex-grow"> <%= image_tag "component_images/demo-dashboard", class: "w-full" %> </div> </div>
<div class="flex"> <%= render 'sidebar' %> <div class="flex-grow"> <%= image_tag "component_images/demo-engineering-team", 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" }があります。sidebar Stimulus controllerのsetCurrent()メソッドが呼ばれますaria: {current: "page|false"}をつけていますbuttonタグのとこにdata-action="click->sidebar#toggle"をつけます。sidebar 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で取得できます。通常使っているStimulus targets(static targetsで指定するもの)は不要となっています。またこのHTML要素のaria-expandedを適宜設定していますsetCurrent(event)は選択されたリンクの背景を灰色にするものです。先にthis.#resetAriaCurrent()で今まで選択されていたものをクリアしたのち、data-actionを受けたHTML要素(event.currentTargetで取得)でaria-current="page"を設定していますdata-turbo-permanentで可能です
aタグをクリックしたと同時にTurbo Driveが自動的に動いていますが、同時にTurboに繋がらない
独立のStimulusも使っています。したがって2番目の緑の経路と、3番目の青の経路を使った感じになっていますHotwireのTurboでもブラウザのステートを柔軟に扱わなければならないケースがあります。1つはTurbo FramesやTurbo Streamsを使い、ステートを変更したくない箇所を迂回する方法です。もう一つは今回のようにdata-turbo-permanentを使って、ステートを変更しない「島」を設定する方法です。また今回は該当しませんでしたが、Morphingを使うこともできます。個々のケースで最良のものを選択します。