EY-Office ブログ

Railsプログラマー向けJSフレームワークStimulusに入門してみた

Rails 7.0でフロントエンド(JavaScript)周りが一新されたという噂は聞いていたのですが、やっと数日前から勉強してみました。

日本語の詳しい記事はなくて直ぐにはわかりませんでしたが、大前提として

  • Rails 7.0で一新されたフロントエンド全体をHotwireと呼ぶようです
  • Hotwireには以下の2(3)つのツールがあります
    • Turbo: JavaScriptを書かなくてもインタラクティブなフロントエンドが作れる
    • Stimulus: Rails的なJavaScriptフレームワーク
    • Stradas : スマホを含むフロントエンド・フレームワーク 2023年中にリリース予定

こののブログではStimulusに入門してみた記事を書きます、またHotwireの概要はHotwireかんたん入門が良くまとまっていると思いました。

Stimulus DeepAIが生成した画像です

Stimulusで「じゃんけん ポン!」

このブログで良くでてくるじゃんけんアプリ、例えばこのブログをStimulusで書いてみました。

通常のRailsアプリであればじゃんけんの勝敗判定等はモデルで行うべきですが、ここでは全てJavaScriptで行います。

開発手順

開発手順は通常のRails手順です、最後の行がStimulusのJavaScriptファイルの生成です。

$ gem install rails
$ rails new jyanken_app
$ cd jyanken_app
$ rails generate scaffold jyanken human:integer computer:integer judgment:integer
$ rails generate stimulus jyanken

HTML(view)ファイル

scaffoldが作ったapp/views/jyankens/index.html.erbファイルを書き換えました。

<%                                   # ↓ ①
  table_style  = "margin-top: 20px; border-collapse: collapse"
  cell_style   = "border: solid 1px #888; padding: 3px 15px"
  div_style    = "margin: 0px 20px"
  button_style = "margin: 0px 10px; padding: 3px 10px; fontSize: 14px"
%>

<div data-controller="jyanken"     <%# ← ② %>
  data-jyanken-scores-value="[]">  <%# ← ③ %>

  <h1>じゃんけん ポン!</h1>

  <div style="<%=div_style%>">      <%# ↓ ④ %>
    <button data-action="click->jyanken#pon" style="<%=button_style%>" value="0">グー</button>
    <button data-action="click->jyanken#pon" style="<%=button_style%>" value="1">チョキ</button>
    <button data-action="click->jyanken#pon" style="<%=button_style%>" value="2">パー</button>
  </div>

  <table style="<%=table_style%>">
    <thead>
      <tr>
        <th style="<%=cell_style%>">あなた</th>
        <th style="<%=cell_style%>">コンピュター</th>
        <th style="<%=cell_style%>">勝敗</th>
      </tr>
    </thead>
    <tbody>
      <% (1..10).each do |n| %>          <%# ← ⑤ %>
        <tr data-jyanken-target="row">   <%# ← ⑥ %>
          <td data-jyanken-target="human" style="<%=cell_style%>"></th>  <%# ←⑦ %>
          <td data-jyanken-target="computer" style="<%=cell_style%>"></th>
          <td data-jyanken-target="judgment" style="<%=cell_style%>"></th>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>
  • ① スタイル(CSS)指定はstyle属性を使います、ここスタイル指定の文字列
  • data-controller属性で対応するJavaScriptコントローラーを指定
  • data-jyanken-scores-valueでJavaScriptで扱うデータを格納するdata属性 scoresの初期値を設定
  • data-action属性でイベント処理を指定
    • ここではclickイベントでjyanken JavaScriptコントローラーのponメソッドが起動されます
    • ボタンの識別はvalue属性の値で指定
  • ⑤ ERBの機能でテーブルの行を10行作っています
    • 動的にStimulusで行(html)を追加する方法が判らなかったので、とりあえず10行作っています
  • data-jyanken-targetで結果を書くなどのDOM操作するエレメントを指定します
    • ここではテーブルの行をJavaScriptでコントロール出来るようしています
    • 複数の行に同じ名前を指定していますがJavaScriptの方からは配列としてアクセスできます
  • ⑦ テーブルの列にも結果を書くのでdata-jyanken-targetを指定しています

JavaScriptコントローラー・ファイル

stimulusが作った app/javascript/controllers/jyanken_controller.js にJavaScriptは書きます。

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { scores: Array }                            // ← ①
  static targets = [ "row", "human", "computer", "judgment" ]  // ← ②

  connect() {                                                  // ← ③
    this.showScores()
   }

   pon(event) {                                                // ← ④
    const human = Number(event.target.value)                   // ← ⑤
    const computer = Math.floor(Math.random() * 3)             // ← ⑥
    const judgment = (computer - human + 3) % 3                // ← ⑦
    const score =  {human, computer, judgment}                 // ← ⑧

    this.scoresValue = [score].concat(this.scoresValue)        // ← ⑨
    this.showScores()                                          // ← ⑩
   }

  showScores() {                                                // ← ⑪
    const TeString = ["グー", "チョキ", "パー"]
    const JudgmentString = ["引き分け", "勝ち", "負け"]
    const scoreLength = this.scoresValue.length

    this.humanTargets.forEach((e, ix) => {                      // ← ⑫
      e.textContent = ix < scoreLength ? TeString[this.scoresValue[ix].human] : ""
    })
    this.computerTargets.forEach((e, ix) => {
      e.textContent = ix < scoreLength ? TeString[this.scoresValue[ix].computer] : ""
    })
    this.judgmentTargets.forEach((e, ix) => {
      e.textContent = ix < scoreLength ? JudgmentString[this.scoresValue[ix].judgment] : ""
    })
    this.rowTargets.forEach((e, ix) => {                       // ← ⑬
      e.hidden = ix >= scoreLength
    })
  }
}
  • ① valuesはHTML上のdata属性をJavaScriptから読み書きできます
    • ここでscoresはじゃんけんの結果が入る配列です
    • 初期値はHTMLのdata-jyanken-scores-value属性で指定されています
    • 参照:Managing State
  • ② targetsはHTMLのDOMを書き換える場所の名前です
    • HTMLファイルのdata-jyanken-target属性に対応する
  • ③ connectメソッドは画面表示時に実行されます
    • ここでは結果表示のshowScoresメソッドを呼び出しています
  • ④ じゃんけんのボタンを押したさいに実行されるメソッド
    • 引数eventはJavaScriptのEventオブジェクトです
  • ⑤ どのボタンが押されているかはevent.target.valueで取得できます
  • ⑥ コンピューター側の手の発生
  • ⑦ 勝敗の判定
  • {人の手, コンピューターの手, 勝敗}のオブジェクト作成
  • ⑨ valuesの更新、最新の結果を
    • this.scoresValue.unshift(score)でも良さそうですが動作しませんでした
  • ⑩ 結果表示のshowScoresメソッドを呼び出し
  • ⑪ 結果表示のshowScoresメソッド
  • data-jyanken-target="human"属性が付いているHTML要素(ここではtd)に人の手の文字列を設定
    • 結果配列の長さ(scoreLength)より小さい場合のみ設定
    • 表示用テーブルの長さは10なのでixは0〜9になります
    • HTML要素(Nodeオブジェクト)は変数eに入っているのでtextContentプロパティーで文字列を設定
  • ⑬ 結果配列が10より小さい時、表示のない行(tr)を非表示にしています
    • hidden属性にtrueを設定すると行(tr)は非表示になります
    • 結果配列が大きくなったときに自動的に行(tr)追加する良い方法が見つからなかったので、このようにしています
      • innerHTMLプロパティーを使えば出来ますが、スマートではなく危険性もあるので使いませんでした

Stimulusまとめ

Ruby on Railsを知っている方には、Railsらしいフレームワークだと感じると思います。

  • RailsではMVC(Model-View-Controller)が分離されていますが、StimulusでもJavaScriptはView(HTML)に書くのではなく、JavaScriptコントローラー(app/javascript/controllers/XXXXXX_controller.js )に書きます
  • HTMLとJavaScriptの間では、主に以下のやり取りがあります
    • action: イベントが発生した際にJavaScriptが呼び出される
    • target: 変更や参照ができるDOM要素の
    • values: HTML上のdata属性を読み書きできます
  • DOMの書き換えは、HTMLではdata-XXXXXX-target属性が設定された要素のみになります
    • また複数の要素に同じ名前が付いている場合は、JavaScriptからは配列として扱えます
  • 上で定義した名前はJavaScript側ではstatic targets = ["yyyy"]のように設定する事でthis.yyyyTargetsのような特別な変数としてアクセスできます
  • HTML要素にdata-action="click->jyanken#pon"のようにイベントが発生した場合に呼び出すJavaScriptコントローラー・メソッドが指定できます

Stimulusを習得するにはStimulusHandbookを読むのが良いと思います(もちろんRuby on RailsとJavaScriptの知識が必要になります)。 さらにReferenceも眺めておくと良いと思います。

日本語の記事もありますが、入門的なものやTuboのものが多いです。

おまけ、じゃんけん ポン!の結果をRDBに格納

上のコードはじゃんけんの結果は保存されませんが、Ruby on Railsを使っているので結果をRDBの格納するようにしてみます。

HTML app/views/jyankens/index.html.erb

<%
  table_style  = "margin-top: 20px; border-collapse: collapse"
  ・・・
%>

<div data-controller="jyanken"
  data-jyanken-scores-value="<%= @jyankens.reverse.to_json %>" > <%# ← ① %>

・・・
  • ① Rails/ERBの機能でRDBに格納されている結果をscoresの初期値に設定します
    • @jyankensには全試合結果が入っています。それを逆順にし、JSON形式の文字列に変換しています
    • data-jyanken-scores-valueでJavaScriptのscoresの初期値が設定できます

JavaScript app/javascript/controllers/jyanken_controller.js

import { Controller } from "@hotwired/stimulus"
import { post } from "@rails/request.js"           // ← ①

export default class extends Controller {
  static values = { scores: Array }
  static targets = [ "row", "human", "computer", "judgment" ]

  connect() {
    this.showScores()
   }

   pon(event) {
    const human = Number(event.target.value)
    const computer = Math.floor(Math.random() * 3)
    const judgment = (computer - human + 3) % 3

    const score =  {human, computer, judgment}
    this.scoresValue = [score].concat(this.scoresValue)

    post("/jyankens", {body: {jyanken: score}, responseKind: "json"})  // ← ②
      .then((response) => { console.log("OK", response) })
      .catch((error) => { console.log("Error", error) })

    this.showScores()
   }

   ・・・
  • ① RailsのAPIをアクセスするための関数をインポート、ここではpost関数
  • ② 結果のscoreオブジェクトをRailsのコントローラーにPOSTしています
    • responseKindを指定しないとHTMLアクセスと解釈され使われないredirect(302)が戻るので、jsonを指定することでcreated(201)が戻るようにしています
    • ここではpostのレスポンスはコンソールに出していますが、レスポンスをチェックした場合は const res = await post()でレスポンスを待つことがきます

Railsのフレームワークなので、Railsとの連携は簡単ですね!

まとめ

現在Ruby on Railsを使ったサービスを作る際に、フロントエンドをどうするかは悩ましいところがあると思います。ReactやVue.jsのようなインタラクティブさは要求されない場合の選択しは、

  1. 従来からあるjQuery等を使う
  2. Stimulusを使う
  3. それでもReactやVue.jsを使う

だと思いますが、ReactやVue.jsの習得はたいへん、そしてjQueryでゴロゴリ書くのも嫌だというRailsプログラマーには良いかもしれませんね。
ただし、日本語の情報はReactやVue.jsにくらべ圧倒的に少ないので覚悟しましょう。😅

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ