はじめに
Railsでstimulusを使う場合は、stimulus-rails gem が使われます。
このgemにより、stimulus controllerを生成できます。
生成されたstimulus controllerは、JavaScriptの管理方法に応じた方法で、登録する必要があります。
JavaScriptの管理方法としてJavaScript bundlerを使う場合は、registerメソッドを使います。
import HelloController from "./hello_controller"
application.register("hello", HelloController)
JavaScriptの管理方法としてimport mapsを使う場合は、eagerLoadControllersFromメソッド or lazyLoadControllersFromメソッドを使います。
// Eager load all controllers defined in the import map under controllers/**/*_controller
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)
// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
lazyLoadControllersFrom("controllers", application)
この記事では、JavaScriptの管理方法としてimport mapsを使う場合に用いる、これらメソッドの仕組みをまとめます。
eagerLoadControllersFromメソッド
eagerLoadControllersFromメソッドは、指定したディレクトリ配下の全てのcontrollerを登録します。
第一引数で、登録するcontrollerがあるディレクトリを指定します。
第二引数は、stimulus controllerを登録する母体みたいなものを渡しています。
export function eagerLoadControllersFrom(under, application) {
const paths = Object.keys(parseImportmapJson()).filter(path => path.match(new RegExp(`^${under}/.*_controller$`)))
paths.forEach(path => registerControllerFromPath(path, under, application))
}
このメソッドの1行目で、HTML上のimportmap定義をパースします。
そして、stimulus controllerに対して定義している部分だけ抽出しています。
例えば、以下のようなimportmap定義があったとします。
<script type="importmap" data-turbo-track="reload">{
"imports": {
"application": "/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js",
"@hotwired/turbo-rails": "/assets/turbo.min-dfd93b3092d1d0ff56557294538d069bdbb28977d3987cb39bc0dd892f32fc57.js",
"@hotwired/stimulus": "/assets/stimulus.min-dd364f16ec9504dfb72672295637a1c8838773b01c0b441bd41008124c407894.js",
"@hotwired/stimulus-loading": "/assets/stimulus-loading-3576ce92b149ad5d6959438c6f291e2426c86df3b874c525b30faad51b0d96b3.js",
"controllers/application": "/assets/controllers/application-368d98631bccbf2349e0d4f8269afb3fe9625118341966de054759d96ea86c7e.js",
"controllers/hello_controller": "/assets/controllers/hello_controller-549135e8e7c683a538c3d6d517339ba470fcfb79d62f738a0a089ba41851a554.js",
"controllers": "/assets/controllers/index-2db729dddcc5b979110e98de4b6720f83f91a123172e87281d5a58410fc43806.js"
}
}</script>
この場合、eagerLoadControllersFromメソッドの1行目は['controllers/hello_controller']として評価されます。
eagerLoadControllersFromメソッドの2行目では、1行目で抽出したstimulus controllerの登録をregisterControllerFromPathメソッドで行なっています。
function registerControllerFromPath(path, under, application) {
const name = path
.replace(new RegExp(`^${under}/`), "")
.replace("_controller", "")
.replace(/\//g, "--")
.replace(/_/g, "-")
if (canRegisterController(name, application)) {
import(path)
.then(module => registerController(name, module, application))
.catch(error => console.error(`Failed to register controller: ${name} (${path})`, error))
}
}
初めに、変数nameに、stimulus controllerの識別子を定義しています。
pathが'controllers/hello_controller'なら、nameに'hello'が定義されます。
importメソッドのところで、stimulus controllerの登録をします。
実際に、登録を行なっているのは、registerControllerメソッドです。
function registerController(name, module, application) {
if (canRegisterController(name, application)) {
application.register(name, module.default)
}
}
このメソッドのregisterメソッドにより、stimulus controllerを登録します。
registerメソッドで登録を行うのは、JavaScriptの管理方法としてJavaScript bundlerを使う時と同様です。
lazyLoadControllersFromメソッド
lazyLoadControllersFromメソッドは、指定したディレクトリ配下のcontrollerを使っているDOMがあったら、そのcontrollerを登録します。
第一引数、第二引数で渡すものeagerLoadControllersFromメソッドと同様です。
export function lazyLoadControllersFrom(under, application, element = document) {
lazyLoadExistingControllers(under, application, element)
lazyLoadNewControllers(under, application, element)
}
このメソッドの1行目のlazyLoadExistingControllersメソッドで、HTML上で既に使われているstimulus controllerを登録しています。
例えば、HTML上で<p data-controller='hello'></p>があれば、hello_controllerを登録します。
2行目のlazyLoadNewControllersメソッドで、動的に使われるようになったstimulus controllerを登録します。
例えば、HTML上に<p></p>があって、それの属性にdata-controller='hello'をJavaScriptで追加した時に、hello_controllerを登録します。
これら2メソッドについてそれぞれまとめます。
lazyLoadExistingControllersメソッド
lazyLoadExistingControllersメソッドは、HTML上のdata-controller属性の値を抽出し、その属性の値のstimulus controllerを登録します。
function lazyLoadExistingControllers(under, application, element) {
queryControllerNamesWithin(element).forEach(controllerName => loadController(controllerName, under, application))
}
queryControllerNamesWithinメソッドで、HTML上のdata-controller属性の値を抽出しています。つまり、stimulus controller名を抽出します。
例えば、HTML上に<div data-controller='card'><p data-controller='hello'></p></div>があれば、'card'と'hello'が抽出されます。
loadControllerメソッドで、queryControllerNamesWithinメソッドで抽出したstimulus controllerを登録します。このメソッドは、eagerLoadControllersFromメソッドの節で説明したregisterControllerFromPathメソッドとほぼ同じことをしています。
lazyLoadNewControllersメソッド
lazyLoadNewControllersメソッドは、HTML上のDOMの動的な変更に応じて、stimulus controllerを登録します。
function lazyLoadNewControllers(under, application, element) {
new MutationObserver((mutationsList) => {
for (const { attributeName, target, type } of mutationsList) {
switch (type) {
case "attributes": {
if (attributeName == controllerAttribute && target.getAttribute(controllerAttribute)) {
extractControllerNamesFrom(target).forEach(controllerName => loadController(controllerName, under, application))
}
}
case "childList": {
lazyLoadExistingControllers(under, application, target)
}
}
}
}).observe(element, { attributeFilter: [controllerAttribute], subtree: true, childList: true })
}
DOMの動的な変更はMutationObserverで監視しています。監視対象は以下です。
-
data-controller属性 - DOMの子ノードの増減
data-controller属性を監視することで、既存DOMのdata-controller属性の値が変わった時にもstimulus controllerが動的に登録されます。
例えば、HTML上に<p data-controller='hello'></p>があって、data-controller属性の値を'goodbye'に変えたらgoodbye_controllerが登録されます。
DOMの子ノードの増減を監視することで、JavaScriptで動的に増えたDOMに対するstimulus controllerが登録されます。
例えば、HTML上に<div data-controller='card'</div>があって、そのDOMの中にJavaScriptで
<p data-controller='hello'></p>を追加したら、hello_controllerが登録されます。
おわりに
Import mapsでJavaScriptを管理しているときのstimulus controller登録の仕組みをまとめました。
Stimulus controllerを登録するメソッドは、eagerLoadControllersFrom、lazyLoadControllersFromがあります。
前者は1度に全てのcontrollerを登録し、後者は使われるcontrollerだけ登録します。
1つの画面でほぼ全てのcontrollerを使う場合はeagerLoadControllersFrom、そうでない場合はlazyLoadControllersFromを使うのが良いかもしれません。