Stimulus v3.0.0で追加されそうな機能


先日(執筆時点では今朝未明)にStimulusのv3.0.0-beta.1が発表されました。

以前DHHのブログではStimulus2.1がもう直ぐ出るよとのアナウンスがありましたが、なぜかいきなり3.0.0 beta.1の発表ってことになっててややびっくりです😲

Stimulus is also due for a 2.1 release in the near future, which will include a helpful debug mode, previous value passing on the ValueChanged callbacks, default values, and a few other overdue enhancements.

最近は色々ありましたがバージョンアップに到れたのは素直に嬉しいですね。
界隈が待望してた機能も多く含まれてると思います!

発表された機能やオプションはそこまで多くもないので一つずつ拾ってみます😋

npmパッケージがstimulusから@hotwired/stimulusになりました。

リポジトリがhotwired orgに移行したのに伴い、パッケージも@hotwired/stimulusになりました。
skypack等で参照してる場合は読み込むurlが変わることになりますのでお気をつけください。

今回のパッケージから必要となるすべてのパッケージが組み込まれており、これまで@stimulus/polyfillsを個別で読み込んでいたものが不要になりました。

アクションのメソッドにパラメータが渡せるようになりました。

アクションにHTML側からパラメータが渡したいけど渡せないということについては長年様々な場所で議論されていましたが、とうとう公式にサポートされることになりました。

HTML側にdata-action属性とともに、data-{controller}-{name}-param="1234"の形式で属性を付与すると、アクションの発火時にパラメータとして渡されます。

パラメータはeventオブジェクトにparamsキーが追加される形で渡ってくるようです。

<button type="button"
  data-action="click->a#method1"
  data-a-hoge-param="1234"
>Fire method1</button>
app.register('a', class extends Controller {
  method1({params}) {
    console.log(params)
  }
});

See the Pen QIITA用_書き捨て by nazomikan (@nazomikan) on CodePen.

型のキャストは以下の対応を取ります。

Data attribute Param Type
data-item-id-param="12345" 12345 Number
data-item-url-param="/votes" "/votes" String
data-item-payload-param='{"value":"1234567"}' { value: 1234567 } Object
data-item-active-param="true" true Boolean

どうやら、ざっくりJSON.parseしてるようですね [差分]

TargetObserverが実装されました。

targetプロパティがMutationObserverで監視され、その生死にあわせて発火するコールバックが追加されました。

targetで指定された要素が追加された(あるいは既存の要素にtargetの属性が付与された)場合に{name}TargetConnectedが、逆に削除された(あるいはtarget属性が外れた)場合に{name}TargetDisconnectedが発火するようになったようです。

これはいくつか嬉しいユースケースがあります(特に以下に限った話ではないのですが)

要素が削除された時「〜をしたい」といったケースってイベントによる実装が難しかったりするのです。

理由は簡単で、削除された要素はDOM中にその時点でいないのでイベントをバブルアップさせることができないからですね。

これまでこのようなケースを自前のMutationObserverによる監視で補う実装パターンが界隈で多く見られましたが、これらの置き換えをこの{name}TargetDisconnectedで実現できるようになるわけです。

具体的に比較コードを用意しました。

以下は要素のカウントを数えるコントローラです。

要素が削除されるごとにカウントを更新したい場合これまでは以下の実装パターンが界隈で多く見られました。

See the Pen QIITA用_書き捨て by nazomikan (@nazomikan) on CodePen.

connect() {
  let observer = new MutationObserver(() => this.updateCount());
  observer.observe(this.element, {childList: true})
  //...
}

この部分ですね

{name}TargetDisconnectedを用いると、以下の例のようにわざわざこのMutationObserverを呼び出す処理を自前で書く必要はなくなります。

See the Pen QIITA用_書き捨て by nazomikan (@nazomikan) on CodePen.

Values APIにデフォルト値が設定できるようになりました。

従来のvaluesは、JavaScript側で宣言する時にキー名とタイプを表現することしかできませんでしたが、今回から値部分をオブジェクト表現することで、デフォルト値を設定することができるようになりました。

従来の指定

static values = {name: String, score: Number}

今回からの指定(従来通りの指定も可能)

static values = {name: String, score: {type: Number, default: 0}}

See the Pen QIITA用_書き捨て by nazomikan (@nazomikan) on CodePen.

ValueChangedコールバックにPreviousValueが渡るようになった

こちらはリリース文書には直接記載はありませんが、xxxValueChangedコールバックに引数が二つ渡るようになりました。

第一引数は現在値、第二引数が前回値です。

直前の値を確認したいという要求はちらほら聞かれていましたがそれが今回から可能になったという感じです。

本件についてはv3.0.0-beta.1のドキュメント内で言及があります。

Classes APIにマルチクラスアクセサのxxxClassesが実装されました。

これはUtility-first CSSのブームに伴い、求める声が高まっていった機能です。

今まで通りの属性指定で、値を複数渡します。

data-{controller}-{name}-class="class1 class2 class3"

そうした時に、JavaScript側でthis.{name}Classesでアクセスするとそれを配列形式で受け取ることができるようになります。

Utility-first CSSとかの場合、一気にn個のクラスを付け替えたいというケースは往々にしてあります。

これはそういった場面に役立ちます。

See the Pen QIITA用_書き捨て by nazomikan (@nazomikan) on CodePen.

Debugモードが導入されました。

開発中、ページにどんなコントローラが埋め込まれており、どんなアクションが発火したのかしばしば可視化できたら嬉しいという場面はあるかと思います。
そういった開発を促進するための情報をコンソールに吐き出してくれるオプションが追加されました。

applicationインスタンスのdebugプロパティをtrueにすると動き出します。

const app = Application.start();
app.debug = true

未知のコントローラやアクション、ターゲットにたいしてwarningが出る様になりました。

以下のパターンに該当する時、コンソールにwarningが出力されるようになりました。

  • data-controller 属性で宣言されたコントローラが存在しないか、アプリケーションに登録されていない場合
  • data-action属性で宣言されたメソッドが、指定されたコントローラに存在しない場合
  • data-action属性で宣言されたコントローラが存在しない場合
  • data-targetまたはdata-[identifier]-targetで宣言されたターゲットが、コントローラのstatic targets配列に存在しない場合
  • data-targetで宣言されたコントローラが存在しない場合
  • data-[identifier]-target 属性が空の場合
  • data-target属性のフォーマットが間違っているか、値が空の場合

これはapplicationインスタンスのwarningsプロパティをfalseにすることで出力を抑制することができます。

const app = Application.start();
app.warnings = false

2021/09/23追記

ついさきほど根本的な問題があったということで一旦revertされました。
表現の仕方的に復活がないというわけではなさそうですが、3系に含まれるかは黄色信号という感じになりました。

コントローラをネストした時の挙動に問題があるという報告があったほか、本日あらたに採用されたshouldLoadとの共存が難しかったためではないかなと推測します。

このrevertは先ほどマージされたのでv3.0.0-rc.1のタイミングでなくなる見込みです。

コントローラにshouldLoadフラグが追加されました (2021/09/23追記)

コントローラの読み込みを制御するためのshouldLoadフラグが追加されました。
コントローラに静的プロパティとしてshouldLoadを定義し、falseに設定するとそのコントローラが読み込まれなくなります。

import { isWebView } from "../browser"

app.register('foo', class extends Controller {
  static shouldLoad = !isWebView(); // webviewの時にこのコントローラを読まれないようにする
})

これはやや奇妙な感じを抱くかもしれません。

なぜならテンプレート側でコントローラ属性の付与を制御すればこのプロパティはそもそも不要となるはずだからです。

これはHTML自体をキャッシュしたいようなケースを想定してJavaScript側だけで読み込みを制御できるように用意されたもののようです。

すでにマージされたのでv3.0.0-rc.1のタイミングでリリースされる見込みです。

コントローラにdispatch関数が実装されました。

Stimulusではコントローラ間の連携にイベントを利用することが推奨されるケースが多々あります。
その際、都度手動でイベント生成、発火までを行う煩わしさを軽減するために、dispatch関数が用意されました。

これまで

open() {
  let evt = new CustomEvent('disclosure:open', {bubbles: true});
  this.element.dispatchEvent(evt);
}

のように書いていたものが

open() {
  this.dispatch('open')
}

で実現できます。 詳細な引数情報は以下に示します。

this.dispatch(eventName, option)

引数 type 説明
eventName String 発生させるイベント名
第二引数のoptionでprefixが明示的に指定されていなければコントローラ名:eventNameというイベント名になる
option Object prefix: イベント名のprefix (default: コントローラ名 = this.identifier)
target: イベントを発生させる要素 (default: コントローラ要素 = this.element)
cancelable: キャンセル可能かどうか (default: true)
bubbles: バブルアップさせるかどうか (default: true)
detail: イベントに渡すデータ (default: {})

と、ここまで記述しましたが、実際に手元でためしてみるとthis.dispatch()では動作しませんでした。
this.context.dispatch()だと期待通り振る舞うみたいですが、PRでの説明やリリース文書と乖離があるのでこちらは現在issueで聞いてる次第です。(私の勘違いだったらゴメンナサイ)

9/1追記
どうやら期待した振る舞いではなかったようで、修正のPRが作られました。
this.dispatch()が想定してたもので間違いないようです。

9/14追記
beta2で修正がリリースされたようです。

IE11のサポートが終了

StimulusのtsconfigのtargetがES5からES2017に変更されており、確認してみたところStimulus3系からはIE11のサポートが打ち切られることになったようです。
本件は3.0.0-beta.1のドキュメント内でも確認できます
確認した際にリリース文書にもその旨の追記していただけたみたいです。

引き続きIE11をサポートするケースにおいてはStimulus2 x @stimulus/polyfillsの組み合わせを使ってくれということみたいです。