🦓

[Rails]セレクトボックスの選択肢を動的に表示する

2023/11/01に公開

はじめに

Turbo FramesとStimulusを使用して、Railsアプリ内でセレクトボックスの選択肢を動的に表示するように実装していきます。

環境

Ruby 3.2.1
Rails 7.0.8

tl;dr

  1. scaffoldでAddressモデルを作成し、DBのマイグレーションを実行する
  2. gem city-stateをインストールする
  3. セレクトフォームを作る
  4. gem requestjs-railsをインストールする
  5. リクエストURLを追加する
  6. Addressesコントローラーに対応するメソッドを追加する
  7. turbo-streamレスポンスを作成する
  8. JSコントローラーを作成する
  9. リファクタリング

scaffoldでAddressモデルを作成する

country, stateを持つAddressモデルを作成します。

➜   rails g scaffold Address country state

DBのマイグレーションを実行します。

gem city-stateをインストールする

Gem
gem 'city-state'
bundle install

city-stateは国、州、都市に関する情報を提供するgemです。
https://github.com/thecodecrate/city-state

セレクトフォームを作る

text_fieldで作成されたフォーム要素をセレクトボックスに変えます。

app/views/addresses/_form.html.erb
# selectコントローラーと接続する
<div data-controller="select">
    <div>
      <%= form.label :country, style: "display: block" %>
      <%= form.select :country, 
      # 国を取得する
      CS.countries.invert, 
      {prompt: '国を選択してください'}, 
      # selectコントローラーのchangeアクションを発火させる
      {data: { action: "change->select#change" }} %>
    </div>
    <div>
      <%= form.label :state %>
      # targetとして指定する
      <%= form.select :state, [], {data: { select_target: "stateSelect" }} %>
</div>

CS.countriesで国とコードのハッシュを取得することができます。

irb(main):001> CS.countries
=> 
{:AD=>"Andorra",
 :AE=>"United Arab Emirates",
 :AF=>"Afghanistan",
 :AG=>"Antigua and Barbuda",
 :AI=>"Anguilla",
 :AL=>"Albania",
 :AM=>"Armenia",
 :AO=>"Angola",
 :AQ=>"Antarctica",
 :AR=>"Argentina",
...

https://railsdoc.com/page/select

gem requestjs-railsをインストールする

requestjs-railsは、RailsでJavaScriptのHTTPリクエストを行うためのラッパーgemです。

➜ bin/importmap pin @rails/request.js
Pinning "@rails/request.js" to https://ga.jspm.io/npm:@rails/request.js@0.0.9/src/index.js
append  app/javascript/application.js

https://github.com/rails/requestjs-rails

リクエストURLを追加する

ajaxリクエスト用URLを追加します。

config/routes.rb
Rails.application.routes.draw do
  resources :addresses do
    collection do
      get :states
    end
  end
end
Prefix Verb URI Pattern Controller#Action
states_addresses GET /addresses/states(.:format) addresses#states

Addressesコントローラーに対応するメソッドを追加する

app/controllers/addresses_controller.rb
class AddressesController < ApplicationController
  def states
    # 更新する部分を指定する
    @target = params[:target]
    # params[:country]に対応する州(states)の情報を取得する
    @states = CS.get(params[:country]).invert
    
    respond_to do |format|
      format.turbo_stream 
    end
  end
end

turbo-streamレスポンスを作成する

@target要素に取得した@statesを表示させます。

app/views/addresses/states.turbo_stream.erb
<%= turbo_stream.update @target do %>
  <%= options_for_select @states %>
<% end %>

JSコントローラーを作成する

countryを選択したらajaxリクエストを送り、選択した国のstatesの値をレスポンスとして受け取ります。

app/javascript/controllers/select_controller.js
import { Controller } from "@hotwired/stimulus"
import { get } from "@rails/request.js"

export default class extends Controller {
  static targets = ["stateSelect"]
  change(event){
   # 選択した国を取得する
    let select = event.target.selectedOptions[0].value;
     # stateを指定する
    let target = this.stateSelectTarget.id;

    get(`/addresses/states?target=${target}&country=${select}`, {
      # 取得したstate値をturbo-streamで返す
      responseKind: "turbo-stream"
    })
  }
}

https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/selectedOptions

Image from Gyazo

Started GET "/addresses/states?country=AD&target=address_state" for 127.0.0.1 at 2023-11-02 14:34:23 +0900
Processing by AddressesController#states as TURBO_STREAM
  Parameters: {"country"=>"AD", "target"=>"address_state"}
  Rendering addresses/states.turbo_stream.erb
  Rendered addresses/states.turbo_stream.erb (Duration: 0.2ms | Allocations: 302)
Completed 200 OK in 1ms (Views: 0.5ms | ActiveRecord: 0.0ms | Allocations: 648)

リファクタリング

セレクトボックスの選択肢を動的に表示するようになったが、再利用性が低いのでリファクタリングします。

valueプロパティを使う

valuesプロパティを使って、コントローラーのurlプロパティを定義します。
static valuesプロパティを使用することで、コントローラーにプロパティ(urlparam)を追加し、それを初期化することができます。
コントローラーが接続されたときに指定されたURLに対してHTTPリクエストを行うことができます。

app/javascript/controllers/select_controller.js
static values = {
  url: String,
  param: String
}

# urlを書き換える
get(`${this.urlValue}?target=${target}&country=${this.paramValue}`, {
  responseKind: "turbo-stream"
})

urlをフォームに定義することによって、コントローラーを複数のフォームに利用されることが可能になり、再利用性が向上します。

app/views/addresses/_form.html.erb
<div data-controller="select" 
data-select-url-value="<%= states_addresses_path %>" 
data-select-param-value="country">

https://stimulus.hotwired.dev/reference/values

URLSearchParams:append()を使う

URLSearchParams インターフェースは、URLの検索パラメーターを操作するための一連のメソッドを提供します。
URLSearchParams インターフェースの append() メソッドは、URLのクエリ文字列内に新しいキーと値を追加するために使用されるメソッドです。

let url = new URL("https://example.com?foo=1&bar=2");
let params = new URLSearchParams(url.search);

// 2番目の foo パラメーターを追加します。
params.append("foo", 4);
// クエリー文字列はこうなる: 'foo=1&bar=2&foo=4'

URLにあるtargetcountryをappend()メソッドを使ってクエリパラメーターとして追加します。

app/javascript/controllers/select_controller.js
change(event){
  let params = new URLSearchParams()
  params.append(this.paramValue, event.target.selectedOptions[0].value)
  params.append("target",this.stateSelectTarget.id)

  get(`${this.urlValue}?${params}`, {
    responseKind: "turbo-stream"
  })
}

https://developer.mozilla.org/ja/docs/Web/API/URLSearchParams/append

終わりに

requestjsを初めて使ってみました。
レスポンスフォーマットをturbo-streamを指定することができて便利です!

Discussion