Rails 7.0でフロントエンド(JavaScript)周りが一新されたという噂は聞いていたのですが、やっと数日前から勉強してみました。
日本語の詳しい記事はなくて直ぐにはわかりませんでしたが、大前提として
- Rails 7.0で一新されたフロントエンド全体をHotwireと呼ぶようです
- Hotwireには以下の2(3)つのツールがあります
こののブログではStimulusに入門してみた記事を書きます、またHotwireの概要はHotwireかんたん入門が良くまとまっていると思いました。
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
属性に対応する
- HTMLファイルの
- ③ 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
プロパティーを使えば出来ますが、スマートではなく危険性もあるので使いませんでした
- hidden属性にtrueを設定すると行(
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を習得するにはStimulusのHandbookを読むのが良いと思います(もちろん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()
でレスポンスを待つことがきます
- responseKindを指定しないとHTMLアクセスと解釈され使われないredirect(302)が戻るので、
Railsのフレームワークなので、Railsとの連携は簡単ですね!
まとめ
現在Ruby on Railsを使ったサービスを作る際に、フロントエンドをどうするかは悩ましいところがあると思います。ReactやVue.jsのようなインタラクティブさは要求されない場合の選択しは、
- 従来からあるjQuery等を使う
- Stimulusを使う
- それでもReactやVue.jsを使う
だと思いますが、ReactやVue.jsの習得はたいへん、そしてjQueryでゴロゴリ書くのも嫌だというRailsプログラマーには良いかもしれませんね。
ただし、日本語の情報はReactやVue.jsにくらべ圧倒的に少ないので覚悟しましょう。😅