6
4

More than 1 year has passed since last update.

たった10行で簡単に絞込みセレクトボックス(Javascript無し)

Last updated at Posted at 2022-02-08

親子セレクトボックス

1番目のセレクトボックス(ドロップダウンリスト)で選択すると、2番目のセレクトボックスで選択肢が絞り込まれる、あれです。(正式にはなんというのかわかりませんでした。ドリルダウン?)(とりあえず、1番目を親セレクタ、2番目を子セレクタと呼びます)

例としては、親セレクタで都道府県名を選択すると、その都道府県内の市町村に子セレクタの選択肢が絞り込まれるというありがちなパターンです。

その親子セレクタが、Rails 7 で採用された TurboHotwire) であれば、ものすごく簡単に実装できます。トータルで10行くらいです。Javascript は使いません(HTML と Javascript の知識は必要。)
環境を Rails 7.0.1 と Ruby 3.1 として、例を作成しながら説明します。
なお、Turbo 自体は Rails に依存しません。→【Hotwire】HTMLだけで分かる Turbo 🚜

window.gif

下準備

rails new drill_down_example
cd drill_down_example
rails g scaffold movie name:string
rails g model character name:string movie:references
app/models/movie.rb
class Movie < ApplicationRecord
  has_many :characters
end

Movie(親モデル)とCharacter(子モデル)の 1 対 N モデルで関連づけ(アソシエーション)を設定します。

db/seeds.rb
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は次のように書き換え。

app/views/movies/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
app/views/movies/show.html.erb
<%= 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を次のように書き換え。

app/controllers/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 が発火しません。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4