Stimulus 2.0 を試してみた

8 min read読了の目安(約7800字

はじめに

2020/12/5 に Stimulus 2.0 がリリースされました 🎉

Stimulus 2.0 リリースノート

以前 Rails アプリで JavaScript を書くときに少しだけ使ったことがあり便利だったので、2.0 も試してみました。

前提

$ ruby -v
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin19]
$ bin/rails -v
Rails 6.1.1

今回はテキストエリアの文字数をカウントする処理を Stimulus で実装してみようと思います。 Rails Girls アプリ・チュートリアル でつくるアイデア投稿アプリをベースにして、アイデアの Description 項目に処理を入れていきます。

参考: How to use Stimulus JS 2.0's new Values and CSS Classes APIs

実装

実装したリポジトリです。 cobachie/railsgirls-next

1. Stimulus をインストール

$ bin/rails webpacker:install:stimulus

上記コマンドでインストールします。

package.json に"stimulus": "^2.0.0" が追加されていますね。

package.json
{
  "name": "railsgirls_next",
  "private": true,
  "dependencies": {
    "@fortawesome/fontawesome-free": "^5.15.1",
    "@rails/actioncable": "^6.0.0",
    "@rails/activestorage": "^6.0.0",
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "4.3.0",
+   "stimulus": "^2.0.0",
    "turbolinks": "^5.2.0"
  },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.11.0"
  }
}

2. Stimulus の controller を用意する

インストールしたときに、app/javascript/controllers/hello_controller.js というサンプルファイルが生成されました。このファイルの名前を idea_controller.js と変えて使用していきたいと思います。

$ mv app/javascript/controllers/hello_controller.js app/javascript/controllers/idea_controller.js
app/javascript/controllers/idea_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "output" ]

  connect() {
    this.outputTarget.textContent = 'Hello, Stimulus!'
  }
}

不要なコメントは削除しておきます。

3. HTML に Stimulus を導入する

アイデアの入力を行う画面 app/views/ideas/_form.html.erb に data-controller = "idea" を挿入することで、app/javascript/controllers/idea_controller.js と接続できます。

data-controller で指定している"idea" は、さきほど作成した idea_controller.js を指しています。

app/views/ideas/_form.html.erb
- <%= form_with(model: idea, local: true) do |form| %>
+ <%= form_with(model: idea, local: true, data: { controller: 'idea' }) do |form| %>
  
  ...

  <div class="field">
    <%= form.label :description %>
    <%= form.text_area :description %>
    
+   <div data-idea-target="output"></div>
  </div>

  ...
  
<% end %>

そして、Description の文字数を表示する場所として <div data-idea-taget="idea"></div> を追加します。data-idea-target には idea_controller.js の static targets として設定されている "output" を指定します。(Stimulus - Targets)

app/javascript/controllers/idea_controller.js を見てみると、connect メソッドで this.outputTarget のテキストに "Hello, Stimulus!" という文字列を入れています。

app/javascript/controllers/idea_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "output" ]

  connect() {
    this.outputTarget.textContent = 'Hello, Stimulus!'
  }
}

この状態で bin/rails s をしてブラウザで見てみましょう。<div data-idea-target="output"></div> の場所に "Hello, Stimulus!" が表示されるのを確認できると思います。

4. Description の文字数をカウントする

data 属性を型付きの値として読み書きできる Values API を使って実装していきます。Values API は Stimulus 2.0 で入った新しい機能ですね。

Stimulus の controller に values プロパティを定義し、Number 型の characterCount を追加します。characterCount は Description の上限文字数として使います。

app/javascript/controllers/idea_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "output" ]

+  static values = {
+    characterCount: Number
+  }

  ...
}

そして HTML の data 属性で characterCount に値 140 を渡します。

app/views/ideas/_form.html.erb
- <%= form_with(model: idea, local: true, data: { controller: 'idea') do |form| %>
+ <%= form_with(model: idea, local: true, data: { controller: 'idea', idea_character_count_value: 140 }) do |form| %>
  ...

Stimulus の controller に渡っているのかを確認するために、以下のようにログを出力する 1 行を入れてブラウザで見てみます。

app/javascript/controllers/idea_controller.js
export default class extends Controller {
  ...

  static values = {
    characterCount: Number
  }

  connect() {
    this.outputTarget.textContent = 'Hello, Stimulus!'
+   console.log(this.characterCountValue)
  }
}

ブラウザの Console に 140 と出力されました。これで HTML の data 属性に指定した値が Stimulus の controller に渡っていることを確認できました。

次に Description に入力された値を取得します。Stimulus で簡単に扱えるように、HTML の Description テキストエリアに "field" という名前の Target 属性を追加しましょう。

app/views/ideas/_form.html.erb
  <div class="field">
    <%= form.label :description %>
-   <%= form.text_area :description %>
+   <%= form.text_area :description, data: { idea_target: "field" } %>

    <div data-idea-target="output"></div>
  </div>

そして Stimulus controller で Description に入力した文字数を取得し、output の要素に "●● characters" と文字数を表示するようにします。

app/javascript/controllers/idea_controller.js
export default class extends Controller {
-  static targets = [ "output" ]
+  static targets = [ "field", "output" ]

  ...

  connect() {
+    let length = this.fieldTarget.value.length
+    this.outputTarget.textContent = `${length} characters`
-    this.outputTarget.textContent = 'Hello, Stimulus!'
  }
}

ブラウザで確認すると 0 characters と表示され、Description に入力しても文字数は変わりません。

それでは Description に入力したときに文字数の表示を更新できるようにしていきましょう。

Description テキストエリアの keyup イベントで idea_controller の change アクションを実行するようにします。

Stimulus - Actions

app/views/ideas/_form.html.erb
  <div class="field">
    <%= form.label :description %>
-   <%= form.text_area :description, data: { idea_target: "field"} %>
+   <%= form.text_area :description, data: { idea_target: "field", action: 'keyup->idea#change' } %>

    <div data-idea-target="output"></div>
  </div>

そして、controller に change アクションを追加し、connect アクションからも change を実行するようにしておきます。 (connect は HTML から Stimulus controller に接続したときに実行されるメソッドで、例えばページをリロードしたときなどに発火します。アイデアの更新時など Description に値が登録されている場合に、画面を表示したら最初から文字数を表示しておけるようにします。)

app/javascript/controllers/idea_controller.js
  connect() {
    this.change()
  }

  change() {
    let length = this.fieldTarget.value.length
    this.outputTarget.textContent = `${length} characters`
  }

これで Description の文字数をカウントし、画面に表示できるようになりました。

5. 文字数制限を超えたら赤色で表示する

characterCount で設定した 140 文字を制限文字数とします。Description の入力文字数が 140 文字を超えた場合に "●● characters" の文字を赤色にするようにしてみます。

140 文字を超えた場合 output element に text-danger という CSS クラスを指定し、140 文字未満の場合は text-danger クラスを除去するようにします。

CSS クラスは contorller で指定せず、Stimulus 2.0 で導入された CSS Classes API を使って HTML から渡すようにします。なお、 text-danger というクラスは Bootstrap のクラス です。(Bootstrap 4.5.0 を使用しています。)

app/javascript/controllers/idea_controller.js
  change() {
    let length = this.fieldTarget.value.length
    this.outputTarget.textContent = `${length} characters`
    
+   if (length > this.characterCountValue) {
+     this.outputTarget.classList.add("text-danger")
+   } else {
+     this.outputTarget.classList.remove("text-danger")
+   }
  }
app/views/ideas/_form.html.erb
- <%= form_with(model: idea, local: true, data: { controller: 'idea', idea_character_count_value: 140 }) do |form| %>
+ <%= form_with(model: idea, local: true, data: { controller: 'idea', idea_character_count_value: 140, idea_over_limit_class: "text-danger" }) do |form| %>

所感

2.0 の 新機能 Values や CSS Classes の便利さをいまいち活かしきれていない気がしますが、変数を controller で定義せず HTML から渡せるようにすることで、controller をより汎用的に利用できるようになったのかなと思います。

Stimulus を使うために必要な知識は多くないので、すぐに使い始められるというのはやっぱりうれしいです。React や Vue.js がフロントエンドの主流になっていますが、アプリの目的や機能によっては Stimulus を使ってシンプルに早く開発するという選択もありだと思いました。