こんにちは。
みなさん流石にそろそろHotwireを使っているのではないでしょうか?
Hotwireを使うと、JavaScriptが少量の記述で済むようになるので、Railsのフロント開発がだいぶ楽になりますよね。
今回は、HotwireのJavaScriptフレームワークであるStimulusのコントローラーをとりあえず作っとけ5選を公開します。
Stimulusとは
Stimulusは、HotwireのJavaScriptフレームワークです。
HotwireのJavaScriptフレームワークというと、TurboとStimulusの2つがあります。
Turboは、RailsのViewをAjaxで更新するためのフレームワークです。
Stimulusは、Turboで更新したViewに対してJavaScriptを適用するためのフレームワークです。
Stimulusでは、コントローラーという単位でJavaScriptを管理します。
このコントローラーの作成時の思想として、「できるだけ分割して、できるだけシンプルにする」というものがあります。
たとえば、モーダル内のタブを切り替えるという画面があったすると、モーダルのコントローラーとタブのコントローラーをそれぞれ分割して作成し、それぞれがそれぞれの役割のみを管理するような設計にすることで再利用性が高まり、コードもシンプルになります。
そこで、今回は汎用性が高いコントローラーを5つ紹介します。
modal_controller
モーダルを表示するためのコントローラーです。
successCloseという、イベント成功時にモーダルを閉じるためのメソッドを持っています。
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
  static targets = [ "modal" ]
  connect() {
    this.modalTarget.classList.remove("hidden")
  }
  successClose(event) {
    if (event.detail.success) {
      this.modalTarget.classList.add("hidden")
    }
  }
  close() {
    this.modalTarget.classList.add("hidden")
  }
  open() {
    this.modalTarget.classList.remove("hidden")
  }
}
previewable_controller
画像をプレビュー表示するためのコントローラーです。
inputされた画像を、previewTargetに表示します。
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
  static targets = [ "input", "preview" ]
  preview() {
    const file = this.inputTarget.files[0]
    this.previewTarget.src = URL.createObjectURL(file)
  }
}
removeable_controller
削除ボタンを押すと、対象の要素を削除するためのコントローラーです。
非表示にするのではなく、DOMとして削除するので、たとえばform内の不要なinputを削除するときに便利です。
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
  static targets = [ "object" ]
  remove() {
    this.objectTarget.remove()
  }
}
disableable_controller
ボタンの無効化を行うためのコントローラーです。
toggleメソッドを呼び出すと、ボタンのdisabled属性を切り替えます。
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
  static targets = [ "button" ]
  connect() {
    this.buttonTargets.forEach((button) => {
      button.disabled = true
    })
  }
  toggle() {
    this.buttonTargets.forEach((button) => {
      button.disabled = !button.disabled
    })
  }
}
tab_controller
タブを切り替えるためのコントローラーです。
タブの切り替えは、タブのクリックイベントを監視して行います。
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
  static targets = ['btn', 'panel']
  static classes = ['hide', 'minor', 'major']
  connect() {
    this.btnTargets[0].setAttribute('aria-selected', 'true')
    this.btnTargets[0].setAttribute('tabindex', '0')
    this.panelTargets[0].classList.remove(...this.hideClasses)
    this.btnTargets[0].classList.remove(...this.minorClasses)
    this.btnTargets[0].classList.add(...this.majorClasses)
  }
  change(evt) {
    let targetBtn = evt.currentTarget
    this.openPanel(targetBtn)
  }
  changeByKey(evt) {
    let btns = this.btnTargets,
        currentIndex = btns.indexOf(evt.currentTarget),
        nextIndex,
        targetBtn
    switch(evt.key) {
      case 'ArrowRight': {
        evt.preventDefault();
        nextIndex = currentIndex === (btns.length - 1) ? 0 : currentIndex + 1
        targetBtn = btns[nextIndex]
        targetBtn.focus()
        this.openPanel(targetBtn)
        break
      }
      case 'ArrowLeft': {
        evt.preventDefault()
        nextIndex = currentIndex === 0 ? (btns.length - 1) : currentIndex - 1
        targetBtn = btns[nextIndex]
        targetBtn.focus()
        this.openPanel(targetBtn)
        break;
      }
    }
  }
  openPanel(targetBtn) {
    let btns = this.btnTargets,
        currentIndex = btns.indexOf(targetBtn),
        panels = this.panelTargets
    this.closeAllPanel()
    targetBtn.setAttribute('aria-selected', 'true')
    targetBtn.setAttribute('tabindex', '0')
    panels[currentIndex].classList.remove(...this.hideClasses)
    btns[currentIndex].classList.remove(...this.minorClasses)
    btns[currentIndex].classList.add(...this.majorClasses)
  }
  closeAllPanel() {
    let btns = this.btnTargets,
        panels = this.panelTargets
    btns.forEach((btn) => {
      btn.setAttribute('aria-selected', 'false')
      btn.setAttribute('tabindex', '-1')
      btn.classList.remove(...this.majorClasses)
      btn.classList.add(...this.minorClasses)
    });
    panels.forEach((panel) => {
      panel.classList.add(...this.hideClasses)
    });
  }
}
まとめ
今回は、汎用性の高いコントローラーを5つ紹介しました。
汎用性の高いcontrollerなので、とりあえず作っておいて必要な時に呼び出す形にしておけば便利かと思います。
また、今回記述した中でもかなり汎用性の高さに違いがあると思います。
tab_controllerで書いているような書き方だと追加するclass要素を汎用性高く書けるので、ぜひ参考にしてみてくださいね。
今日はこのあたりで。

![[Rails] StimulusJSとTOAST UIでカレンダーを表示してみる](/assets/ogp/calendar-with-stimulusjs-and-toast-ui.webp)