Stimulusをはじめよう

## Stimulusとは何か

[Stimulus](https://stimulus.hotwire.dev/)とは、JavaScriptで書かれたクライアントサイドのライブラリです。 
Basecampによって開発され、2021/12にversion2がリリースされました。
皆さんの知るところでいうと[stack overflow](https://stackoverflow.design/product/guidelines/javascript/)が採用していたりします。

StimulusはHTMLを中心に考え、JavaScirptで書かれた振る舞いをHTMLから呼び出すことができるように設計されています。
それはonClickでJavaScriptコードを呼び出していた太古のコードに少し似ています。
さぁ、私と共にWeb開発を再発見しましょう！


## 基本原理

StimulusはHTMLに書かれた振る舞いを与えます。
それは最初にレンダリングされたHTMLだけでのことではなく、後から挿入されたHTMLに対しても同じです。

Stimulusは[MutationObserver](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)を利用してDOMの変更を常に監視し、振る舞いを与えるべきHTMLが検出された時、即座にアタッチするようにできています。

## HELLO WORLD

まずはStimulusを読み込みます。
難しく構える必要はありません、最も簡単な方法はCDNで読み込むことです。

```js
<script type="module">
  import {Controller, Application} from "https://cdn.skypack.dev/stimulus@2.0.0";
</script>
```

Stimulusを読み込むとApplicationとControllerという2つのクラスが提供されます。
ApplicationはHTMLを監視し、振る舞いをアタッチすることを責務としたクラスであり、Controllerは振る舞いを定義するために用いるクラスになります。

まずはApplicationクラスを使ってHTMLの監視を開始しましょう、それは`start`メソッドを呼び出すだけです。

```js
let app = Application.start();
```

次にControllerクラスを使って振る舞いを定義しましょう。　Controllerを継承したクラスを作り、それをappに登録するとすぐに使えるようになります。

```js
app.register('hello', class extends Controller {
  connect() {
    alert('HELLO WORLD')
  }
})
```

`connect`はStimulusが最初から提供するLifecycle Callbackです。 (LifeCycleCallbackにはinitialize, connect, disconnectの3つがあります。)

振る舞いを与える対象の要素がHTML中に現れたらその度に発火します。

これで準備が整いました。
さっそくHTMLからこの振る舞いを呼び出してみましょう

```html
<div data-controller="hello"></div>
```

HTML側に`data-controller`属性を付与すると、Stimulusがそれを検知し、対応する名前で登録されたcontrollerをアタッチします。

これによりconnectライフサイクルコールバックが呼び出されアラートがでます。

[完全なコード(codepen)](https://codepen.io/nazomikan/pen/gOLdrWd)


## イベントに合わせて振る舞いを与える

StimulusではHTML側に`data-action`属性を付与することでclickやinputといったイベントに対する振る舞いを呼び出すことができるようになります。

```html
<div data-controller="notifier">
  <button type="button" data-action="notifier#notify">click me</button>
</div>
```

`data-action`属性の値は`{event}->{controller}#{method_name}`のフォーマットで記述します。
対応する振る舞いを記述しましょう。

```js
app.register('notifier', class extends Controller {
  notify(evt) {
    alert('HELLO WORLD')
  }
})
```

これでclick meボタンをクリックした際にnotify関数が呼び出すことができるようになります。

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


## 要素にアクセスする

StimulusではHTML側に`data-{controller}-target="{name}"`属性を付与し、値に設定した`{name}`をJavaScript側の静的プロパティに設定すると`{name}Target`というgetterが生成され、アクセスできるようになります。

```html
<div data-controller="hello">
  <p data-hello-target="world">hello world</p>
  <button type="button" data-action="click->hello#reverse">reverse</button>
</div>
```

```js
app.register('hello', class extends Controller {
  static targets = ['world'];

  reverse(evt) { // 文字を反転させる例
    this.worldTarget.textContent = this.worldTarget.textContent
      .split('').reverse().join('');
  }
})
```

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

また、`data-{controller}-target="{name}"`を指定した要素が複数ある場合は`{name}Targets`という名前で配列としてアクセスすることもできます。

## 状態を持つ

UIが状態を持つことはよくあります。 モーダルが開いてる時・閉じてる時、ディスクロージャーが開いてる時・閉じてる時など。
Stimulus version2にはそういった状態をHTML側にもつためのAPIも提供されています。

`data-controller`を指定した要素に対して`data-{controller}-{name}-value="{value}"`属性を付与し、属性名中の{name}をJavaScript側の静的プロパティ`values`に型と共に設定すると`{name}Value`というgetter/setterが生成され、アクセスできるようになります。


```html
<div data-controller="counter"
  data-counter-num-value="0"
>
  <p data-counter-target="view">0</p>
  <button type="button" data-action="click->counter#plus1">+1</button>
</div>
```

```JavaScript
app.register('counter', class extends Controller {
  static targets = ['view'];
  static values = {num: Number}; 

  plus1(evt) {
    this.viewTarget.textContent = ++this.numValue;
  }
})
```

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


上記のコードはHTML中の`data-counter-num-value`の値にthis.numValueという値でアクセスできるようになり、++によりsetter経由でdata属性の値を更新し、その結果をtextContentに流し込むことで状態を持つことができているわけです。

`static values`にはObjectを指定します。　キーがvalue名({name})になり、値がそのデータ型のコンストラクタになります。
(この情報を元にthis.xxxValueでアクセスしたときに自動的にキャストされた状態でアクセスできるようになるわけです)

このValue APIはさらに奥深い機能を持っています。

それはValueChangedCallbackとよばれる機能で、`{name}ValueChanged`という名前の関数を定義しておくと、valueの値がsetter経由で変更された場合にコールバックを実行してくれるというものになります。

これをもとにさきほどのコードをリファクタリングすると以下のようにかけます。(より恩恵を感じるためにminus1ボタンも実装してみましょう。)

```HTML
<div data-controller="counter"
  data-counter-num-value="0"
>
  <p data-counter-target="view">0</p>
  <button type="button" data-action="click->counter#plus1">+1</button>
  <button type="button" data-action="click->counter#minus1">-1</button>
</div>
```

```JavaScript
app.register('counter', class extends Controller {
  static targets = ['view'];
  static values = {num: Number};

  plus1(evt) {
    ++this.numValue;
  }
  
  minus1(evt) {
    --this.numValue;
  }
  
  numValueChanged() {
    this.viewTarget.textContent = this.numValue;
  }
})
```

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


より洗練されたコードになりました。

`plus1`/`minus1`ではvalueを変更するだけにとどめ、その変更により呼ばれたコールバック内でviewを変更させることにより、ビジネスロジックからDOM操作を排除することに成功しています。


## Classを管理する

Stimulusの最後のAPIはClass APIです。　これはさほど難しい概念ではありません。

ディスクロージャーが開いた、モーダルがオープンした。　そういう状態を見た目に反映させるためにclassをHTMLに振りたいときは度々あります。

そういったclassをJavaScriptに直で書いてしまうケースはこれまで度々あったのではないでしょうか？

しかしながら、StimulusはHTMLを中心においたライブラリです。 HTMLのもつべき状態はHTML中に存在させるべきです。

Class APIはそれを実現するために、HTML中に記述しておいたclass名にJavaScriptからプロパティとしてアクセスするためのAPIです。

data-controllerを指定した要素に対して`data-{controller}-{name}-class="{className}"`という属性を付与し、属性名中の{name}をJavaScript側の静的プロパティclassesに設定すると{name}Classというgetterが生成され、{className}にアクセスできるようになります。

さきほどのValues APIのコードを流用して、counterの値が10を超えたら赤色を付与するコードを書いてみましょう。

```HTML
<div data-controller="counter"
  data-counter-num-value="0"
  data-counter-over10-class="red"
>
  <p data-counter-target="view">0</p>
  <button type="button" data-action="click->counter#plus1">+1</button>
  <button type="button" data-action="click->counter#minus1">-1</button>
</div>
```

```JavaScript
app.register('counter', class extends Controller {
  static targets = ['view'];
  static values = {num: Number};
  static classes = ['over10']

  plus1(evt) {
    ++this.numValue;
  }
  
  minus1(evt) {
    --this.numValue;
  }
  
  numValueChanged() {
    this.viewTarget.textContent = this.numValue;
    this.element.classList.toggle(this.over10Class, this.numValue >= 10)
  }
})
```

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

ここでは`over10`という名前でClass APIを利用しました。 

`numValue`が変更された時に動く`numValueChanged`コールバック時に、`numValue`が10を超えていたら、起点要素に`over10Class`というgetterプロパティを経由して`red`クラスをつけたりはずしたりするように実装されています。

これは非常に単純な作用に見えますが、非常に奥深く、機能の汎化に役立ちます。

JavaScript内で直に特定のclassの付与してしまっていては、このcontrollerは同名のclassをふる時にしか再利用できませんが、class apiを利用することでHTML側を変えるだけでJavaScriptはそのまま再利用できるという汎用性を手にできるのです。

## 最後に

以上でStimulusのもつAPIについての説明は終わりになります。

え？ これだけしかAPIないの？ それで実装困らないの？　と思うかもしれません。
しかしこれが全てです。

覚えることが少なく、学習コストが低いのもStimulusの大きなメリットの一つと言えるでしょう。

今後はStimulusを用いた実装のコツについて書いていきたいと思います。

それでは幸せなStimulus Lifeを。

## links

[Stimulusの実装の勘所 - primitive controller -](https://qiita.com/nazomikan/items/a5f0813ddc63bc5a2708)