親子セレクトボックス
1番目のセレクトボックス(ドロップダウンリスト)で選択すると、2番目のセレクトボックスで選択肢が絞り込まれる、あれです。(正式にはなんというのかわかりませんでした。ドリルダウン?)(とりあえず、1番目を親セレクタ、2番目を子セレクタと呼びます)
例としては、親セレクタで都道府県名を選択すると、その都道府県内の市町村に子セレクタの選択肢が絞り込まれるというありがちなパターンです。
その親子セレクタが、Rails 7 で採用された Turbo(Hotwire) であれば、ものすごく簡単に実装できます。トータルで10行くらいです。Javascript は使いません(HTML と Javascript の知識は必要。)
環境を Rails 7.0.1 と Ruby 3.1 として、例を作成しながら説明します。
なお、Turbo 自体は Rails に依存しません。→【Hotwire】HTMLだけで分かる Turbo 🚜
下準備
rails new drill_down_example
cd drill_down_example
rails g scaffold movie name:string
rails g model character name:string movie:references
class Movie < ApplicationRecord
has_many :characters
end
Movie(親モデル)とCharacter(子モデル)の 1 対 N モデルで関連づけ(アソシエーション)を設定します。
movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }, { name: "Batman"}])
Character.create(name: "Alfred Pennyworth", movie: movies.third)
Character.create(name: "Aragorn II", movie: movies.second)
Character.create(name: "Boromir", movie: movies.second)
Character.create(name: "Bruce Wayne", movie: movies.third)
Character.create(name: "Catwoman", movie: movies.third)
Character.create(name: "Frodo Baggins", movie: movies.second)
Character.create(name: "Leia", movie: movies.first)
Character.create(name: "Luke", movie: movies.first)
Character.create(name: "Robin", movie: movies.third)
サンプルデータを準備します。
rails db:create; rails db:migrate; rails db:seed
ここまでが下準備
本題
view: index
scaffold で作られたindex.html.erb
は次のように書き換え。
<h1>Movies</h1>
<div id="movies">
<%= select :movie, :name, @movies.map{|r| [r.name, movie_path(r.id)]}, {}, {onchange:"Turbo.visit(value)"} %> <%# ① %>
<%= turbo_frame_tag 'children' do %> <%# ② %>
<%= collection_select :movie, :name, @characters, :id, :name %> <%# ③ %>
<% end %> <%# ④ %>
</div>
①:親セレクタの部分です。onchange:"Turbo.visit(value)"
がポイントです。location.href=value
ではなくTurbo.visitでTurboを発火させます。(Turbo はリンクをクリックするかフォームを submit するかで発火するのが原則らしい。)
なので、value の値はPath(URL)
です。
②:子セレクタの受け皿です。Turbo の要。’children’ の部分がIdentifier
になっていて、個々の<turbo-frame>
が区別されます(ここでは一つだけしかないけど)
ここが、アクションによって差し替えられたりなどされます。
③:子セレクタの初期状態です。実際には、空配列か disabled にするのでしょうが、例なので、初期状態では全てのデータが見られるようにしています。理屈上はセレクタボックスですらなくてもよいです。
④:受け皿だけでよければ、ブロックではない<%= turbo_frame_tag "children" %>
でもよいです。
view: show
scaffold で作られたshow.html.erb
を次のように書き換え。パーシャルでやってもよい(本来ならそっちかも)。ここでは、「あるデータを表示したら、それに関連したデータも表示する」という思考に直裁的なので、GETメソッドの典型例 show を使いました。ルーティングでいうと
Helper | HTTP Verb | Path | Controller#Action |
---|---|---|---|
movie_path | GET | /movies/:id(.:format) | movies#show |
<%= turbo_stream.update 'children' do %> <%# ⑤ %>
<%= collection_select(:movie, :name, @characters, :id, :name) %> <%# ⑥ %>
<% end %> <%# ⑦ %>
⑤:children
を手がかりに、②の turbo-frame の内容をupdate
するというアクションになります。Turbo の要です。
【図解】Hotwire : Turbo 7つのアクションが詳しくてわかりやすいです。
⑥:子セレクタの部分です。select タグはform_with
の中で使うのが通常だろうと思います。が、この時点ではモデルと紐付けされていません。なのでf.collection_select
は使えません。
ここで関連付けされたモデルのデータで子セレクタを構成しています。
⑦:アクションによってはブロックでなくてもよい(例えばturbo_stream.remove
)
controller
scaffold で作成されたmovies_controller.rb
を次のように書き換え。
class MoviesController < ApplicationController
before_action :set_movie, only: %i[ show ]
def index
@movies = Movie.all
@characters = Character.all # ⑧
end
def show
render layout: false, content_type: "text/vnd.turbo-stream.html" # ⑨
end
private
# Use callbacks to share common setup or constraints between actions.
def set_movie
@movie = Movie.find(params[:id])
@characters = @movie.characters # ⑩
end
# Only allow a list of trusted parameters through.
def movie_params
params.require(:movie).permit(:name)
end
end
⑧:初期状態の設計方針によってはいらない。
⑨:<turbo-frame>
部分だけ置き換えられるようなレスポンスになります。これがないと、ブラウザは show.html に丸ごと置き換えてしまいます。Trubo のポイント。
⑩:show.html.erb に渡すためのデータを構成してます
最後に、rails assets:precompile
して、rails s
して、http://localhost:3000/movies
で確認。
HTMLソース
初期状態のソースはこうなります
<body>
<h1>Movies</h1>
<div id="movies">
<select onchange="Turbo.visit(value)" name="movie[name]" id="movie_name">
<option value="/movies/1">Star Wars</option>
<option value="/movies/2">Lord of the Rings</option>
<option value="/movies/3">Batman</option>
</select>
<turbo-frame id="children">
<select name="movie[name]" id="movie_name">
<option value="1">Alfred Pennyworth</option>
<option value="2">Aragorn II</option>
<option value="3">Boromir</option>
<option value="4">Bruce Wayne</option>
<option value="5">Catwoman</option>
<option value="6">Frodo Baggins</option>
<option value="7">Leia</option>
<option value="8">Luke</option>
<option value="9">Robin</option>
</select>
</turbo-frame>
</div>
</body>
アップデートされるソースの例(http://localhost:3000/movies/2)
<turbo-stream action="update" target="children">
<template>
<select name="movie[name]" id="movie_name">
<option value="2">Aragorn II</option>
<option value="3">Boromir</option>
<option value="6">Frodo Baggins</option>
</select>
</template>
</turbo-stream>
補足
Turbo (Hotwire) の概念・大枠の把握は Hotwireとは何なのか?
がとてもわかりやすかったです。
もう少し複雑な処理が必要であればStimulus
で Javascript を使う方法もあります。その場合も、遷移はTurbo.visit()
を使わなければ Turbo が発火しません。