Railsの技: StimulusJSのコントローラを"self-destructing"にする(翻訳)
本記事は、Boring Railsの新シーズンコンテンツ「Hotwire Summer」の一部です。
UXを改善するためにJavaScriptをほんのちょっぴり加える必要に迫られることがたまにあります。昔のフルスタック開発者は、そういうときにjQueryを以下のようにページに直接追加することがよくありました。
<script type="application/javascript">
$(".flash-container").delay(5000).fadeOut()
$(".items").last().highlight()
</script>
もちろんこれでも動きますが、ベストとは言えません。
Hotwireアプリでは、"self-destructing"なStimulusコントローラを使えば同じことができます。
self-destructingとは?
"self-destructing"なStimulusコントローラは、何かコードを少し実行してからthis.element.remove()
を呼び出すことで、そのコントローラ自体をDOMから削除します。以下のコード例を見てみましょう。
// app/javascript/controllers/scroll_to_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static values = {
location: String
}
connect() {
this.targetElement.scrollIntoView()
this.element.remove()
}
get targetElement() {
return document.getElementById(this.locationValue)
}
}
このコントローラはlocation
の値を受け取ると、その要素までページをスクロールします。
<template
data-controller="scroll-to"
data-scroll-to-location-value="task_12345"></template>
私は、self-destructingなコントローラを上のように<template>
タグで使うのが好みです。その理由は、<template>
タグがブラウザに一切表示されないのと、コードを読むときに空のdiv
ではないことを明確に示せるからです。
このパターンは、Turbo Streamレスポンスで使うのに最適です。
たとえば、新しいタスクを作成するインラインフォームにタスクのリストが表示されているとします。このフォームを送信して受け取った<turbo-stream>
をリストに追加し、今追加した新しいタスクの位置までスクロールできるようになります。
<%= turbo_stream.append :tasks, @task %>
<%= turbo_stream.append :tasks do %>
<template
data-controller="scroll-to"
data-scroll-to-location-value="<% dom_id(@task) %>"></template>
<% end %>
JavaScriptの小さな機能をStimulusコントローラにラップしてあるので、ライフサイクルイベントはStimulusがすべて面倒を見てくれます。turbo:load
をリッスンしなくても普通に動きます。
他の使いみち
1. highlighterコントローラ
highlighter
コントローラを使うと、以下のように選択中の項目にスタイルを追加できます。
<template
data-controller="highlighter"
data-highlighter-marker-value="<%= dom_id(task, :list_item) %>"
data-highlighter-highlight-class="text-blue-600 bg-blue-100"></template>
Stimulusのvalues
APIとclasses
APIを両方使っているので、このコントローラの再利用性が非常に高くなります。任意のDOM要素のidや任意のCSSクラスを用いて、ハイライトしたい要素を指定できます。
// app/javascript/controllers/highlighter_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static values = {
marker: String
}
static classes = ["highlight"]
connect() {
this.markedElement.classList.add(...this.highlightClasses)
this.element.remove()
}
get markedElement() {
return document.getElementById(this.markerValue)
}
}
2. grab-focusコントローラ
grab-focus
コントローラを用いて、フォームにタスクを手軽に追加できるようになります。フォームから送信するとタスクが作成され、新しい<form>
が次のタスクとして動的に追加されます。このコントローラを使うと、ブラウザのフォーカスを滑らかに次のinputに移動できます。
// app/javascript/controllers/grab_focus_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static values = {
selector: String
}
connect() {
this.grabFocus()
this.element.remove()
}
grabFocus() {
if (this.hasSelectorValue) {
document.querySelector(this.selectorValue)?.focus()
}
}
}
3. アクセス分析用「ビーコン」
このアイデアはHEYから拝借したもので、私はこれをページ分析のトラッキングに用いています。beacon
をページに追加してバックエンドにビーコンを送信し、ページアクセス件数を記録したら、beacon
自身を削除します。
(その気になればBeacon Web APIも使えますが、ここでは簡単にしたいのでPATCHリクエストを送信するだけにとどめています)
// app/javascript/controllers/beacon_controller.js
import { Controller } from '@hotwired/stimulus'
import { patch } from '@rails/request.js'
export default class extends Controller {
static values = { url: String }
connect() {
patch(this.urlValue)
this.element.remove()
}
}
APIをクリーンにしたいので、私たちはこれをRailsビューヘルパーでラップしました。
module AnalyticsHelper
def tracking_beacon(url:)
tag.template data: { controller: "beacon", beacon_url_value: url }
end
end
<!-- Inside app/views/layouts/plan.html.erb -->
<%= tracking_beacon(url: plan_viewings_path(@plan)) %>
まとめ
"self-destructing"なStimulusコントローラは、Hotwireを拡張する素晴らしい方法です。クライアント側の機能をいったん完全に撤去してから丸ごと再構築しなくても、JavaScriptの振る舞いをほんのわずか追加するだけで済みます。機能を小さく単一目的にしておけば、他のページやコンテキストで楽に再利用できるようになります。
Stimulusコントローラに備わっている既存のライフサイクルを利用することで、Turbo Streamsでコンテンツを変更したりTurbo Driveでページ間を移動したりするときの動作を保証できるようになります。
本記事が役に立つと思った方は、ぜひフォームでニュースレターの登録をお願いします。余分な文章を削ぎ落とした読みやすく価値ある情報だけをニュースレターで配信いたします。
概要
原著者の許諾を得て翻訳・公開いたします。