はじめに
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
を使うのが良いかもしれません。