1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

HotwireAdvent Calendar 2023

Day 8

Import mapsでJavaScriptを管理しているときのstimulus controller登録の仕組み

Last updated at Posted at 2023-12-05

はじめに

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を登録するメソッドは、eagerLoadControllersFromlazyLoadControllersFromがあります。
前者は1度に全てのcontrollerを登録し、後者は使われるcontrollerだけ登録します。
1つの画面でほぼ全てのcontrollerを使う場合はeagerLoadControllersFrom、そうでない場合はlazyLoadControllersFromを使うのが良いかもしれません。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?