Stimulusとは何か
Stimulusとは、JavaScriptで書かれたクライアントサイドのライブラリです。
Basecampによって開発され、2021/12にversion2がリリースされました。
皆さんの知るところでいうとstack overflowが採用していたりします。
StimulusはHTMLを中心に考え、JavaScirptで書かれた振る舞いをHTMLから呼び出すことができるように設計されています。
それはonClickでJavaScriptコードを呼び出していた太古のコードに少し似ています。
さぁ、私と共にWeb開発を再発見しましょう!
基本原理
StimulusはHTMLに書かれた振る舞いを与えます。
それは最初にレンダリングされたHTMLだけでのことではなく、後から挿入されたHTMLに対しても同じです。
StimulusはMutationObserverを利用してDOMの変更を常に監視し、振る舞いを与えるべきHTMLが検出された時、即座にアタッチするようにできています。
HELLO WORLD
まずはStimulusを読み込みます。
難しく構える必要はありません、最も簡単な方法はCDNで読み込むことです。
<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
メソッドを呼び出すだけです。
let app = Application.start();
次にControllerクラスを使って振る舞いを定義しましょう。 Controllerを継承したクラスを作り、それをappに登録するとすぐに使えるようになります。
app.register('hello', class extends Controller {
connect() {
alert('HELLO WORLD')
}
})
connect
はStimulusが最初から提供するLifecycle Callbackです。 (LifeCycleCallbackにはinitialize, connect, disconnectの3つがあります。)
振る舞いを与える対象の要素がHTML中に現れたらその度に発火します。
これで準備が整いました。
さっそくHTMLからこの振る舞いを呼び出してみましょう
<div data-controller="hello"></div>
HTML側にdata-controller
属性を付与すると、Stimulusがそれを検知し、対応する名前で登録されたcontrollerをアタッチします。
これによりconnectライフサイクルコールバックが呼び出されアラートがでます。
イベントに合わせて振る舞いを与える
StimulusではHTML側にdata-action
属性を付与することでclickやinputといったイベントに対する振る舞いを呼び出すことができるようになります。
<div data-controller="notifier">
<button type="button" data-action="notifier#notify">click me</button>
</div>
data-action
属性の値は{event}->{controller}#{method_name}
のフォーマットで記述します。
対応する振る舞いを記述しましょう。
app.register('notifier', class extends Controller {
notify(evt) {
alert('HELLO WORLD')
}
})
これでclick meボタンをクリックした際にnotify関数が呼び出すことができるようになります。
See the Pen QIITA_STIMULUS_HELLO_ACTION by nazomikan (@nazomikan) on CodePen.
要素にアクセスする
StimulusではHTML側にdata-{controller}-target="{name}"
属性を付与し、値に設定した{name}
をJavaScript側の静的プロパティに設定すると{name}Target
というgetterが生成され、アクセスできるようになります。
<div data-controller="hello">
<p data-hello-target="world">hello world</p>
<button type="button" data-action="click->hello#reverse">reverse</button>
</div>
app.register('hello', class extends Controller {
static targets = ['world'];
reverse(evt) { // 文字を反転させる例
this.worldTarget.textContent = this.worldTarget.textContent
.split('').reverse().join('');
}
})
See the Pen QIITA_STIMULUS_HELLO_TARGET by nazomikan (@nazomikan) on CodePen.
また、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が生成され、アクセスできるようになります。
<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>
app.register('counter', class extends Controller {
static targets = ['view'];
static values = {num: Number};
plus1(evt) {
this.viewTarget.textContent = ++this.numValue;
}
})
See the Pen QIITA_STIMULUS_HELLO_VALUES by nazomikan (@nazomikan) on CodePen.
上記のコードは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ボタンも実装してみましょう。)
<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>
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;
}
})
See the Pen QIITA_STIMULUS_HELLO_VALUES2 by nazomikan (@nazomikan) on CodePen.
より洗練されたコードになりました。
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を超えたら赤色を付与するコードを書いてみましょう。
<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>
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)
}
})
See the Pen QIITA_STIMULUS_HELLO_CLASS by nazomikan (@nazomikan) on CodePen.
ここでは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を。