Stimulus v3.2.2で追加された機能・変更された振る舞い

執筆時点で昨夕Stimulus v3.2.2がリリースされました。

パッチバージョンではありますが、内容がパッチバージョンとは言えないものが多いので一応拾っておこうと思います。

https://github.com/hotwired/stimulus/releases/tag/v3.2.2

## 機能の追加・変更

### keyboard event filterに`PageUp`/`PageDown`が追加された

Stimulusにはもともとキーボードイベントのフィルタリング機構があります。

https://stimulus.hotwired.dev/reference/actions#keyboardevent-filter

下記のようにアクション記述のイベント名を書くところのキーボードイベント(keydown,keyup,keypress)にドットで続けてキー名を書けば、対象のキーが押された時だけ実行されるアクションとして定義することができる機能です。

```html
<xxx data-action="keydown.left->tab#prev">
```

そこに`PageUp`、`PageDown`のキーをデフォルトで指定できるようになりました

構文中では`page_up`, `page_down`という名前で指定するようです。

https://github.com/hotwired/stimulus/pull/677/files#diff-f9956bb079b5b819f1612c693d20462d21cd15e3ff6fe59f2dc876c9bc5de4afR97-R98

キーボード操作を実現する際、`PageUp`/`PageDown` ではなく `ArrowUp`/`ArrowDown`を使うケースが多いのですが、Web特有のUIなどでたまに求められることもあるのでデフォルトで用意しよう...ということなのかなと思います。

参考(Feedパターンなど):

https://www.w3.org/WAI/ARIA/apg/patterns/feed/#keyboardinteraction


### Number型のValueの値に`_`を含めることができるようになった

StimulusにはHTML側からStimulusコントローラ内で参照したい値を受け渡せるようにするためのValuesと呼ばれるAPIがあります。

```html
<div data-controller="foo" data-foo-some-value="123" />
```

```javascript
static values = {some: Number}

connect() {
  alert(this.someValue); // 123
}
```

コントローラ側で指定した型指定(上記のコードだとNumber)に従ってAPI(this.someValue)にアクセスした際にパースされた結果を得ることができるようになっています。

今回の変更はこのHTML側の記述で、Number型の値の記述だけ`_`を含めることができるようになったというものです。

主に桁を把握しやすく記述するための配慮のようのです

```html
<div data-controller="foo" data-foo-some-value="123_456" />
```

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="PoxgeRd" data-user="Takuya-Nakajima" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Takuya-Nakajima/pen/PoxgeRd">
  Untitled</a> by Takuya Nakajima (<a href="https://codepen.io/Takuya-Nakajima">@Takuya-Nakajima</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


### Clickイベントにもフィルタリング機構が追加された

冒頭の差分紹介時にキーボードイベントのフィルタリングの機構があるという話をしましたが、それと似た機能がクリックイベントにも追加されました。

主にコントロールキーを押しながらのクリックや、シフトキーを押しながらのクリックなどをサポートする目的の変更になります。

対応しているキーは`meta`, `ctrl`, `alt`, `shift`の4キーのようです。

キーの組み合わせは`+`を使います。

```html:例
<button type="button"
  data-controller="foo"
  data-action="ctrl+click->foo#notify"
/>
```

### outletで指定されたセレクタが変更された際にoutletAPIの参照するコントローラも対応したものになるようになった。

Stimulusには要素に指定されたコントローラを使役的に操作したい目的でコントローラのインスタンスにアクセスするためのOutletと呼ばれるAPIが存在しています。

下記のコードのようにfooコントローラを割り当てた要素にhogeコントローラを割り当てた要素のセレクタを`data-{自コントローラ}-{参照したいコントローラ}-outlet="{セレクタ}"`という記述で指定すると`{参照したいコントローラ名}Outlet`というプロパティ名でアクセスできるようになります。

```html
<div
  data-controller="foo"
  data-foo-hoge-outlet="#hoge"
/>
<div
  id="hoge"
  data-controller="hoge"
/>
```

このoutlet記述で指定しているセレクタ(上記のコードでは`#hoge`)が変更された際、これまではその変更が反映されることはなかったのですが、今回の変更によって反映されるようになりました。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="jOQRxeg" data-user="Takuya-Nakajima" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Takuya-Nakajima/pen/jOQRxeg">
  Untitled</a> by Takuya Nakajima (<a href="https://codepen.io/Takuya-Nakajima">@Takuya-Nakajima</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


### afterLoadハンドラ内のthisが未定義だったバグが治った

Stimulusにはコントローラが定義(`application.register`)されたタイミングでafterLoadと呼ばれる関数を発火させるという機能があります(要素にアタッチするタイミングではないことに注意)

このafterLoad内での`this`が`undefined`になっていたバグがあったのですが、これが解消されてコントローラクラスを指すようになりました。

### afterLoadハンドラの第二引数に`application`インスタンスが渡るようになった

上述のafterLoadハンドラは第一引数に定義された時のコントローラ名がわたるのですが、今回それに加えapplication(Application.startの返り値)が渡るようになりました。

上述のafterLoadのコンテキスト変更の差分にしれっと入っており意図は語られていないのですが、サンプルコードにapplicationインスタンスを利用した実装があります。

https://stimulus.hotwired.dev/reference/controllers#trigger-behaviour-when-a-controller-is-registered

この例では特定のクラスにマッチする要素にコントローラを割り当てたいが、`data-controller`というキーワードはapplication.schemaで変更可能なので信頼性を高めるためにapplication.schemaを参照してキーワードを取得する...といった目的で使われてるようです。

稀に嬉しいことがあるのかな〜という感想です。



### outletAPIをconnect内で参照した際に、参照したコントローラが未定義になる問題が一定解消した。

多分これが今回の本命のPRなのかなと思います。

要素の解釈順の問題で以下のような構造ではconnectのタイミングでoutletを正しく参照できませんでした。

```html
<div controller="foo" data-foo-hoge-outlet="#hoge">
  <div controller="hoge" id="hoge">
  </div>
</div>
```

```javascript
application.register('foo', class extends Controller) {
  static outlets = ['hoge'];
  connect() {
    alert(this.hogeOutlet); // undefined
  }
}
```

これはコントローラの割り当てが外の要素から始まることに起因しており、fooコントローラがアタッチされたタイミングではまだ#hogeにhogeコントーラがアタッチされていないため、fooのconnect内では参照が失敗するというものになります。

必然といえば必然なのですが、この挙動に対するissueが乱立したこともあって、一定の対応がとられたようです。
対応内容はoutlet参照時に対象のコントローラが存在しなかった場合、子要素を探索して要素をアタッチするという処理を割り込ませるというものになります。

これにより、上記のコードでも正常に`hoge`コントローラにアクセスできるようになります。

ただ、Stimulusをrailsで利用していたり、またそれを参考にしてautoloadingしている場合、アタッチは非同期的になるのでかわらず動作は保証されないことになります。

https://github.com/hotwired/stimulus-rails/tree/main/app/assets/javascripts

ということで本解消というよりは一定の解消を目指した変更という感じでしょうか。

簡単にawaitするための機構を用意するとかそういう対応がとられるのかなと思っていたので少し驚きです。

下記はoutletで参照してるコントローラのidentifierをconnect時に書き出す処理をバージョン比較したものです。

stimulus3.2.1までの挙動

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="wvQZXaL" data-user="Takuya-Nakajima" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Takuya-Nakajima/pen/wvQZXaL">
  Untitled</a> by Takuya Nakajima (<a href="https://codepen.io/Takuya-Nakajima">@Takuya-Nakajima</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

stimulus3.2.2からの挙動

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="oNQOyzy" data-user="Takuya-Nakajima" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Takuya-Nakajima/pen/oNQOyzy">
  Untitled</a> by Takuya Nakajima (<a href="https://codepen.io/Takuya-Nakajima">@Takuya-Nakajima</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


### registerActionOptionのコールバックに渡ってくるeventオブジェクトのparamsフィールドが参照できるようになる

Stimulusのイベントのフィルタリング機構にActionOptionと呼ばれるものがあります。

https://stimulus.hotwired.dev/reference/actions#options

このActionOptionは独自定義することもでき、たとえばdetails要素の開閉イベントの`toggle`を、openした時だけに限定してアクションを実行させたい時、以下のようなActionOptionを定義しておくと簡単に実現できます。

```javascript:ActionOption
application.registerActionOption("open", ({ event }) => {
  if (event.type == "toggle") {
    return event.target.open == true
  } else {
    return true
  }
})
```

```html
<details
  data-controller="foo"
  data-action="toggle->foo#notify:open"
/>
```

このActionOptionを定義する`registerActionOption`関数は第一引数にActionOption名、第二引数に登録するフィルタリング関数を定義するのですが、このフィルタリング関数に渡る引数オブジェクト内のイベントプロパティに変更がありました。

もともとイベントプロパティにはイベントオブジェクトが渡ってきており、そこに変更はないのですが、StimulusのActionParams(event.params)が利用可能な状態となって渡ってくるようになりました。

https://stimulus.hotwired.dev/reference/actions#options#:~:text=event.params

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="QWJPBbg" data-user="Takuya-Nakajima" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Takuya-Nakajima/pen/QWJPBbg">
  Untitled</a> by Takuya Nakajima (<a href="https://codepen.io/Takuya-Nakajima">@Takuya-Nakajima</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


### registerActionOptionのコールバックに渡ってくるデータにコントローラのインスタンスが追加される

前述の`registerActionOption`のコールバック関数の引数オブジェクトに`controller`フィールドが追加され、このイベントを受信しているコントローラのインスタンスが渡るようになりました。

```html
<button type="button" data-action="click->some#method:option">
```

上記のような記述の場合、controllerフィールドには`some`コントローラのインスタンスが渡ります。

これにより、コントローラ側のvaluesAPIなどにアクセスできるようになるため、より柔軟なフィルタリングが可能になります。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="jOQRpmY" data-user="Takuya-Nakajima" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Takuya-Nakajima/pen/jOQRpmY">
  Untitled</a> by Takuya Nakajima (<a href="https://codepen.io/Takuya-Nakajima">@Takuya-Nakajima</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

上記はくじ引きアプリケーションです。

あたりくじ(5)を引き当てたときだけalertがでるようなフィルタリングをしており、当たりくじの値はvaluesAPIを使って柔軟に設定できるようになっています。

ただ、この機能を使うと特定のコントローラに依存したActionOptionとなりやすいので、関連性をわかりやすくするために前述のafterLoad内で定義するなどしたほうがよいかもしれませんね。(ちょうどapplicationインスタンスにもアクセスできるようになってるので)

## 終わりに

やはりどうみてもパッチバージョンの変更じゃないですねw

outletの挙動変更(主にconnect内での動作保証)が注目度の高い変更なのかなと思いますが、autoloader対応など課題は残されているので今後に注目です。

個人的にはActionOptionに渡るイベントオブジェクトのparamsが解釈可能になった変更が一番ありがたい変更だったかな〜と思います。