コード例
ここで作るのは下記のようなUIです。
2つの方法で作ります。デモはこちらです: StimulusバージョンとCheckboxバージョンです。
aria-checkedも設定しまう)class属性をStimulus Controllerの中から変更しますdata-controller=を記載した、Stimulus controllerが接続された要素を選択しますaria-checkedを選択します。将来的にデザインを変更しても、ここは変わらないからです:checkedを使えば、CSSだけでトグルを左右に動かしたり、背景をグレイから青に変更できます今回は3-bのやり方を紹介し、その後にStimulus controllerを使わないやり方を紹介します。
なお私は3-bのやり方はCQRSの考え方に似ている1と思っていて、とても気に入っています。
<% set_breadcrumbs [["Toggle Stimulus", component_path(:toggle)]] %> <%= render 'template', title: "Toggle Stimulus", description: "Toggle implemented with Stimulus" do %> <!-- Enabled: "bg-indigo-600", Not Enabled: "bg-gray-200" --> <div class="text-center"> <button type="button" class="group bg-gray-200 aria-checked:bg-indigo-600 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" data-controller="switch" data-action="click->switch#toggle keydown.space:stop:prevent->switch#toggle" > <span class="sr-only">Use setting</span> <!-- Enabled: "translate-x-5", Not Enabled: "translate-x-0" --> <span aria-hidden="true" class="translate-x-0 group-aria-checked:translate-x-5 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" ></span> </button> </div> <% end %>
import { Controller } from "@hotwired/stimulus" export default class extends Controller { connect() { } toggle() { this.element.ariaChecked = this.element.ariaChecked === "true" ? "false" : "true" } }
<button>タグで実装しています。data-controller="switch"でstimulus controllerを接続しますdata-action="click->switch#toggle keydown.space:stop:prevent->switch#toggle"により、このトグルはマウスのクリックおよびスペースキーに応答するようになります。スペースキーでも使えるはアクセシビリティの要件ですdata-actionにより、stimulus controllerのtoggle()が呼び出されます。ここではaria-checked属性を"true"/“false"の間で切り替えています<button>タグのCSS classのaria-checked:bg-indigo-600により、aria-checked="true"の時だけボタンの背景が青く表示されるようになります<span aria-hidden="true" ...>の要素はトグルの真ん中の丸いところで、これは左右に動く必要があります。これはgroup-aria-checked:translate-x-5で実装できますHotwireの考え方は、なるべくブラウザのネイティブな機能を活かし、それを拡張していくというものです。バランスよくHTML/CSS/JavaScriptの機能を使っていきます。それに対してReactを含めたJavaScriptヘビーなアプローチでは、ブラウザ機能をJavaScriptで置き換えていく傾向があります。Hotwireでこれをやってしまうと無駄にJavaScriptが多くなってしまいますので、頭を切り替えた方が良いでしょう。
昔からあるやり方ですが、ブラウザのネイティブな機能をさらに活かして、JavaScriptを全く使わないアプローチもあります。HTMLのチェックボックス要素を使うものです。
<% set_breadcrumbs [["Toggle Checkbox", component_path(:toggle)]] %> <%= render 'template', title: "Toggle Checkbox", description: "Toggle implemented with a Checkbox" do %> <!-- Enabled: "bg-indigo-600", Not Enabled: "bg-gray-200" --> <div class="text-center"> <label class="group has-[:checked]:bg-indigo-600 bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer select-none rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out outline-none has-[:focus]:ring-2 active:ring-2 ring-indigo-600 ring-offset-2" role="switch" > <input type="checkbox" class="opacity-0 w-0 border-none"/> <span class="sr-only">Use setting</span> <!-- Enabled: "translate-x-5", Not Enabled: "translate-x-0" --> <span aria-hidden="true" class="group-has-[:checked]:translate-x-5 translate-x-0 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" ></span> </label> </div> <% end %>
:checked擬似セレクタを使ってCSSから読み取れます:has擬似セレクタと組み合わせると、チェックボックスステートに応じてトグル全体の表示をCSSだけで切り替えられますCQRSはGreg Youngなどが2010年に提唱したアーキテクチャでデータの更新と読み出しを単に分けるのを超えて、全く分離して異なる仕組み・構造にするものです。ここで紹介しているやり方は、一方でJavaScriptによってステート(aria-checked属性)を書き込みます(command)。他方ではCSSだけでステートを読み出しています(query)。よってCQRS的なパターンではないかと思います。
これに対してReactは自在にHTMLを変更できますので、同じコンポーネントの中でステートを更新し、かつHTML要素を書き換えることになりがちです。その結果として不必要な依存性が生まれやすいのではないかと思います。 ↩