この投稿は、
DMM WEBCAMP Advent Calendar 2023
シリーズ2 投稿2日目のエントリーです。
1日目は @yuki82511988 さんで
【Ruby】+ と<<で配列の要素を追加した時の話でした。
Rubyは詳しければ詳しい程幸せになれますね。
はじめに
DMM WEBCAMP でメンターをやらせていただいております。 @tomoaki-kimura です。
Rails7のリリースから随分経ちますが、Rails6の環境もまだまだあります。
ということで、今年の寄稿はRails6で改めて動的なフォームの処理に触れてみたいと思います。
環境
- Ruby 3.1.2
- Rails 6.1.4
- yarn 1.22.18
作るもの
- 動的に選択肢の内容が変化するフォーム。
- 地方区分を選ぶと都道府県の選択肢が変わり、都道府県を選択すると政令指定都市の選肢が変化。
- 地方区分を選択しない場合、都道府県及び、政令指定都市の選択は行えない。
- 都道府県は、
dataset
を用いてデーターをdata-json
から参照。 - 政令指定都市は、
Ajax
を用いてデーターをDBから参照。
準備
今回は、政令指定都市に関して以下のようなデーターを作って、入力フォームを作成していきます。
地方区分(areas)
id | name |
---|---|
1 | 北海道 |
2 | 東北 |
3 | 関東 |
4 | 東海 |
5 | 近畿 |
6 | 中国 |
7 | 四国 |
8 | 九州 |
都道府県(prefectures)
id | area_id | name |
---|---|---|
1 | 1 | 北海道 |
2 | 2 | 宮城県 |
3 | 3 | 神奈川県 |
4 | 3 | 埼玉県 |
5 | 3 | 千葉県 |
6 | 4 | 新潟県 |
7 | 4 | 愛知県 |
8 | 4 | 静岡県 |
9 | 5 | 大阪府 |
10 | 5 | 兵庫県 |
11 | 5 | 京都府 |
12 | 6 | 岡山県 |
13 | 6 | 広島県 |
14 | 8 | 福岡県 |
15 | 8 | 熊本県 |
政令指定都市(city)
id | prefecture_id | name |
---|---|---|
1 | 1 | 札幌市 |
2 | 2 | 仙台市 |
3 | 3 | 横浜市 |
4 | 3 | 川崎市 |
5 | 3 | 相模原市 |
6 | 4 | さいたま市 |
7 | 5 | 千葉市 |
8 | 6 | 新潟市 |
9 | 7 | 名古屋市 |
10 | 8 | 静岡市 |
11 | 8 | 浜松市 |
12 | 9 | 大阪市 |
13 | 9 | 堺市 |
14 | 10 | 神戸市 |
15 | 11 | 京都市 |
16 | 12 | 岡山市 |
17 | 13 | 広島市 |
18 | 14 | 北九州市 |
19 | 14 | 福岡市 |
20 | 15 | 熊本市 |
プロジェクトの作成
まずはプロジェクトを作成します。 DBもSQLiteを使います。
(6.1.4 でバージョン指定していますが、6系であれば表記や動作の違い等の問題は起こらないと思われます。)
下記コマンドを実行して下さい。
$ gem install rails -v 6.1.4
$ rails _6.1.4_ new form_test
長いので略
├─ select-hose@2.0.0
├─ selfsigned@1.10.14
├─ serve-index@1.9.1
├─ serve-static@1.15.0
├─ signal-exit@3.0.7
├─ sockjs-client@1.6.1
├─ sockjs@0.3.24
├─ spdy-transport@3.0.0
├─ spdy@4.0.2
├─ strip-eof@1.0.0
├─ thunky@1.1.0
├─ toidentifier@1.0.1
├─ url-parse@1.5.10
├─ utils-merge@1.0.1
├─ uuid@3.4.0
├─ wbuf@1.7.3
├─ webpack-dev-middleware@3.7.3
├─ webpack-dev-server@3.11.3
├─ websocket-driver@0.7.4
├─ websocket-extensions@0.1.4
└─ ws@6.2.2
✨ Done in 4.00s.
Webpacker successfully installed 🎉 🍰
プロジェクトが作成後
$ cd form_test
でディレクトリを移動しておきましょう。
モデルの作成
では、早速今回必要なモデルを作ってしまいましょう。下記コマンドで作成します。
$ rails g model area name
$ rails g model prefecture area:references name
$ rails g model city prefecture:references name
$ rails g model area name
Running via Spring preloader in process 13883
invoke active_record
create db/migrate/20231129134732_create_areas.rb
create app/models/area.rb
invoke test_unit
create test/models/area_test.rb
create test/fixtures/areas.yml
$ rails g model prefecture area:references name
Running via Spring preloader in process 14050
invoke active_record
create db/migrate/20231129134738_create_prefectures.rb
create app/models/prefecture.rb
invoke test_unit
create test/models/prefecture_test.rb
create test/fixtures/prefectures.yml
$ rails g model city prefecture:references name
Running via Spring preloader in process 14232
invoke active_record
create db/migrate/20231129134744_create_cities.rb
create app/models/city.rb
invoke test_unit
create test/models/city_test.rb
create test/fixtures/cities.yml
モデルが作成出来たら、
$ rails db:migrate
をしておきましょう。
Running via Spring preloader in process 14408
== 20231129134732 CreateAreas: migrating ======================================
-- create_table(:areas)
-> 0.0010s
== 20231129134732 CreateAreas: migrated (0.0010s) =============================
== 20231129134738 CreatePrefectures: migrating ================================
-- create_table(:prefectures)
-> 0.0015s
== 20231129134738 CreatePrefectures: migrated (0.0016s) =======================
== 20231129134744 CreateCities: migrating =====================================
-- create_table(:cities)
-> 0.0012s
== 20231129134744 CreateCities: migrated (0.0012s) ============================
続けて、アソシエーションを書いておきます。
class Area < ApplicationRecord
has_many :prefectures #追記
end
class Prefecture < ApplicationRecord
belongs_to :area
has_many :cities #追記
end
データーの作成
今回は決まったデーターを扱いますので、seedを作成しておきます。
下記のコードを seeds.rb
に記述しましょう。
data = { '北海道': { '北海道': %w(札幌市) },
'東北': { '宮城県': %w(仙台市) } ,
'関東': { '神奈川県': %w(横浜市 川崎市 相模原市),
'埼玉県': %w(さいたま市),
'千葉県': %w(千葉市), },
'東海': { '新潟県': %w(新潟市),
'愛知県': %w(名古屋市),
'静岡県': %w(静岡市 浜松市) },
'近畿': { '大阪府': %w(大阪市 堺市),
'兵庫県': %w(神戸市),
'京都府': %w(京都市) },
'中国': { '岡山県': %w(岡山市),
'広島県': %w(広島市) },
'四国': {},
'九州': { '福岡県': %w(福岡市 北九州市),
'熊本県': %w(熊本市) } }
data.each do |area_name, v|
area = Area.create!(name: area_name)
v.each do |prefecture_name, city_names|
prefecture = area.prefectures.create!(name: prefecture_name)
city_names.each do |city_name|
prefecture.cities.create!(name: city_name)
end
end
end
書けたらデーターを流し込みます。
$ rails db:seed
エラーが出なければOKです。
$ rails db:seed
Running via Spring preloader in process 15839
コントローラー
コントローラーに入ります。下記コマンドを実行します。
$ rails g controller Posts new
今回特に何か実際に投稿する訳ではないので、一旦 new
のみテンプレートを作成ました。
では、次にコントローラーの中身を見てみましょう。
class PostsController < ApplicationController
def new
end
end
new
アクションのみ存在しますので、search_cities
を追加します。
また、必要な処理も書いておきましょう。
class PostsController < ApplicationController
def new
@areas = Area.all
prefectures = Prefecture.all
#prefecturesの値を、data-jsonに渡すために必要な項目だけを配列要素にする。
@prefectures_json = prefectures.map{|o| o.attributes.symbolize_keys.select{|k,v| k.match(/(id|_id|name)\z/) } }
#Prefectureの絞り込み パラメーターがないと空配列にしかならない。post押下後に
値を渡す。
@prefectures = prefectures.where(area_id: params[:area_id])
#Cityの絞り込み。パラメーターがないと空配列にしかならない。post押下後に値を渡す。
@cities = City.where(prefecture_id: params[:prefecture_id])
end
def search_cities
@cities = City.where(prefecture_id: params[:prefecture_id])
end
end
@prefecture_json
でモデルのデーターを加工していますが、下記コードの結果は
Prefecture.all.map{|o| o.attributes.symbolize_keys.select{|k,v| k.match(/(id|_id|name)\z/) } }
以下のような、Hashを要素とする配列となります。 json
型にはしていません。
処理としては、複数要素のモデルオブジェクトを配列化し、
irb(main):001:0> Prefecture.all.map{|o|o}
(0.3ms) SELECT sqlite_version(*)
Prefecture Load (0.1ms) SELECT "prefectures".* FROM "prefectures"
=>
[#<Prefecture:0x000000010a914f78
id: 1,
area_id: 1,
name: "北海道",
created_at: Wed, 29 Nov 2023 13:53:33.681664000 UTC +00:00,
updated_at: Wed, 29 Nov 2023 13:53:33.681664000 UTC +00:00>,
#<Prefecture:0x000000010a946730
id: 2,
area_id: 2,
name: "宮城県",
(略)
その中身を attributes.symbolize_key
でキーをシンボルとするHash変換し、
irb(main):002:0> Prefecture.all.map{|o|o.attributes.symbolize_keys}
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures"
=>
[{:id=>1,
:area_id=>1,
:name=>"北海道",
:created_at=>Wed, 29 Nov 2023 13:53:33.681664000 UTC +00:00,
:updated_at=>Wed, 29 Nov 2023 13:53:33.681664000 UTC +00:00},
{:id=>2,
:area_id=>2,
(略)
さらに、 select
で必要なデーターのみ抽出するために、キーの末尾\z
に id
または、_id
または name
と付いた条件をマッチさせています。
irb(main):003:0> Prefecture.all.map{|o| o.attributes.symbolize_keys.select{|k,v|
k.match(/(id|_id|name)\z/) } }
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures"
=>
[{:id=>1, :area_id=>1, :name=>"北海道"},
{:id=>2, :area_id=>2, :name=>"宮城県"},
{:id=>3, :area_id=>3, :name=>"神奈川県"},
{:id=>4, :area_id=>3, :name=>"埼玉県"},
{:id=>5, :area_id=>3, :name=>"千葉県"},
{:id=>6, :area_id=>4, :name=>"新潟県"},
{:id=>7, :area_id=>4, :name=>"愛知県"},
{:id=>8, :area_id=>4, :name=>"静岡県"},
{:id=>9, :area_id=>5, :name=>"大阪府"},
{:id=>10, :area_id=>5, :name=>"兵庫県"},
{:id=>11, :area_id=>5, :name=>"京都府"},
{:id=>12, :area_id=>6, :name=>"岡山県"},
{:id=>13, :area_id=>6, :name=>"広島県"},
{:id=>14, :area_id=>8, :name=>"福岡県"},
{:id=>15, :area_id=>8, :name=>"熊本県"}]
これによって、絞り込みに area_id
と、 option タグに必要な、 id
と name
も取り出せました。
ルーティング
続いてルーティングです。 routes.rb
を開きましょう。
Rails.application.routes.draw do
get 'posts/new'
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
現状このようになっていますので、下記のように書き換えてしまいましょう。
Rails.application.routes.draw do
root 'posts#new'
post 'posts/search_cities'
end
一応確認しておきます。
$ rails routes -c posts
Prefix Verb URI Pattern Controller#Action
root GET / posts#new
posts_search_cities POST /posts/search_cities(.:format) posts#search_cities
良さそうです。ここまでが下準備となります。
ビュー
今度はViewsです
posts/new.html.erb
を編集しましょう。
<h1>Posts#new</h1>
<p>Find me in app/views/posts/new.html.erb</p>
現状このようになっていますので、下記のように書き換えます。
<h1>Posts#new</h1>
<%= form_with url: root_path, local: true, method: :get do |f| %>
地域区分:<%= f.collection_select :area_id, @areas, :id, :name, { include_blank: "-----選択して下さい-----", selected: params[:area_id] }, { id: "areaSelect" } %><br>
都道府県:<%= f.collection_select :prefecture_id, @prefectures, :id, :name, { include_blank: "-----選択して下さい-----", selected: params[:prefecture_id] }, { id: "prefectureSelect", data: { json: @prefectures_json } } %><br>
指定都市:<span id="cities"><%= render 'posts/select_city', cities: @cities %></span><br>
<%= f.submit "POST" %>
<% end %>
~
次にAjax用に切り出している _select_city.html.erb
を 新規作成
します。
<%= collection_select "", :city_id, cities, :id, :name, { include_blank: "-----選択して下さい-----", selected: params[:city_id] }, { id: "citySelect" } %>
ここで、各フォームの違いを解説しておきたいと思います。
最初に選択する Area
フォームですが、 collection_select
を使って、コントローラーから Area
の全件をインスタンス変数 @areas
を入れておきます。
<%= f.collection_select :area_id, @areas, :id, :name, {}, { id: "areaSelect" } %>
Area
の選択を受けて変更される @prefectures
の内容は、 初期値ではコントローラーからモデルを渡されていますが、選択肢の内容はJSで変化させます。 data-json
の中に @prefecture_json
として全件入れておき、ここからデーターを絞り込みます。
<%= f.collection_select :prefecture_id, @prefectures, :id, :name, {}, { id: "prefectureSelect", data: { json: @prefectures_json } } %>
@prefecture_json
は値が変更されるものではないので、コントローラー側で作成していますが、このやりとりのメリットとして、 to_json
等の変換作業を行わなくて良いので、エスケープ文字の処理を考える必要がない点です。
最後の City
ですが、こちらはAjaxで内容を変更しますので、 prefecture_id
を使って絞り込んだデーターを cities: @cities
として渡し、js.erb
ファイルからレンダリングを行います。
<%= collection_select, "", :city_id, cities, :id, :name, {}, { id: "citySelect" } %>
f.collecton_select
を使っていない理由は、Ajax時に f
の値を渡すのが面倒な為です。
ここまでで、フォームの準備は完了です。
JQueryの導入
ここからは、JQueryでの実装となります。
以下のコマンドでJQueryをインストールしましょう。
$ yarn add jquery
yarn add v1.22.18
[1/4] 🔍 Resolving packages...
⠂ jquery(node:24943) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ jquery@3.7.1
info All dependencies
└─ jquery@3.7.1
✨ Done in 1.03s.
次にライブラリの呼び出しをします。
ProvidePluginの設定
config/webpack/environment.js
ファイル内に、
const { environment } = require('@rails/webpacker')
module.exports = environment
グローバルに使う変数を追記しておきましょう。
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jQuery: 'jquery/src/jquery'
})
)
module.exports = environment
ライブラリのimport
ファイルのインポートもやっておきましょう。
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
Rails.start()
Turbolinks.start()
ActiveStorage.start()
こちらを、以下のように追記しましょう。
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
import "jquery"
import "./select_form"
Rails.start()
Turbolinks.start()
ActiveStorage.start()
JQueryを使ったAjax
上記で追加した ./select_form
のファイルは存在しませんので作っておきます。
以下のファイルを新規作成して下さい。
$(document).on('turbolinks:load', () => {
//セレクトボックスの中身を空にする関数
const initSelect = ($target) => {
//セレクトボックスの中身を空にする
$target.empty()
//非選択時のオプションタグを追加する
$target.append(`<option>-----選択して下さい-----</option>`)
}
//セレクトボックスの中身を第二引数 data の要素を持った選択肢にする
const setSelect = ($target, data) => {
//引数 data の値をループで1件ずつ取得し、o として取り出す
$.each(data, (i, o) => {
//対象となるセレクトボックス($target)に対してオプションタグを追加
$target.append(`<option value="${o.id}">${o.name}</option>`)
})
}
//都道府県フォームからJSONデーターを取得
// `data('json')`はjsで書くと`dataset.json`で、DOMの`data-json`の値を取る事ができる
const prefecturesData = $('#prefectureSelect').data('json')
//都道府県データーを地域区分IDから絞り込む関数
const setPrefectureOptions = (areaId) => {
//地域区分IDから絞り込みを行い、変数 result に代入
const result = prefecturesData.filter(o => o.area_id == areaId)
initSelect($('#prefectureSelect'))
setSelect($('#prefectureSelect'), result)
}
//地域区分の値に変化があれば実行するイベント
$('#areaSelect').on('change', (e) => {
//イベントの戻り地 `e` から選択された値を取得し、変数areaIDに代入
const areaId = e.target.value
setPrefectureOptions(areaId)
initSelect($('#citySelect'))
})
//都道府県の値に変化があれば実行するイベント
$('#prefectureSelect').on('change', (e) => {
const prefectureId = e.target.value
//JQueryのajaxを使ってリクエストを行う dataプロパティに渡したい値を格納する
$.ajax('posts/search_cities', {
type: 'post',
data: { prefecture_id: prefectureId }
})
})
})
ここでフォームの制御を行いますが、関数があまり長くならないように、役割で分割していくと分かりやすいです。
概ねの役割をコメントにしているので参考にして下さい。
JQuery
を使うメリットとして、こういった単純な処理の記述の短さや、 $.ajax
で簡単に非同期の POST
が行える点です。
JQueryを使わない環境では、axos
等を使う事になります。
レンダリング
では、最後に $.ajax
で投げられたリクエストを捌く posts/search_cities.js.erb
を新規作成しておきます。
$('#cities').html('<%=j render 'posts/select_city', cities: @cities %>')
動作確認
では確認していきましょう。
$ rails s
でサーバーを起動して、トップページを確認してみましょう。
もしこのようなエラーとなった場合、一旦サーバーを落として下さい。
ActionView::Template::Error (Webpacker can't find application.js in /Users/kimuratomoaki/Desktop/form_test/public/packs/manifest.json. Possible causes:
1. You want to set webpacker.yml value of compile to true for your environment
unless you are using the `webpack -w` or the webpack-dev-server.
2. webpack has not yet re-run to reflect updates.
3. You have misconfigured Webpacker's config/webpacker.yml file.
4. Your webpack configuration is not creating a manifest.
Your manifest contains:
{
}
):
7: <%= csp_meta_tag %>
8:
9: <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
10: <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
11: </head>
12:
13: <body>
app/views/layouts/application.html.erb:10
こちらは、 babel.config.js
で読まれているライブラリーをインストールする事で解決します。
(babel v7.22.0によるエラーのようです。)
解決には以下のコマンドを実行して下さい。
$ yarn add @babel/plugin-proposal-private-methods @babel/plugin-proposal-private-property-in-object
yarn add v1.22.18
[1/4] 🔍 Resolving packages...
⠂ @babel/plugin-proposal-private-methods(node:25953) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
warning @babel/plugin-proposal-private-methods@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.
warning @babel/plugin-proposal-private-property-in-object@7.21.11: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
warning "@babel/plugin-proposal-private-methods > @babel/helper-create-class-features-plugin@7.23.5" has unmet peer dependency "@babel/core@^7.0.0".
warning "@babel/plugin-proposal-private-property-in-object > @babel/plugin-syntax-private-property-in-object@7.14.5" has unmet peer dependency "@babel/core@^7.0.0-0".
warning " > @babel/plugin-proposal-private-methods@7.18.6" has unmet peer dependency "@babel/core@^7.0.0-0".
warning " > @babel/plugin-proposal-private-property-in-object@7.21.11" has unmet peer dependency "@babel/core@^7.0.0-0".
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 2 new dependencies.
info Direct dependencies
├─ @babel/plugin-proposal-private-methods@7.18.6
└─ @babel/plugin-proposal-private-property-in-object@7.21.11
info All dependencies
├─ @babel/plugin-proposal-private-methods@7.18.6
└─ @babel/plugin-proposal-private-property-in-object@7.21.11
✨ Done in 3.71s.
インストール出来たら確認してみましょう。
$ rails s
再度起動して、トップページを確認してみましょう。
都道府県の選択時にのみAjaxが発動しますので、パラメーターが変化しているのが分かると思います。
これで完成です。
お疲れ様でした。
おわりに
ちょっと駆け足でしたが、 data-xxxx
の使い方と、 Ajax
の使い方を見ていきました。
いかがだったでしょうか?
引き続き DMM WEBCAMP Advent Calendar 2023 をよろしくお願いいたします。
次回 3日目の投稿も、私 @tomoaki-kimura で、
Rails6 で動的なセレクトボックスを作りたい(Stimulus x Axios編)
となっています。
当記事の続きモノで、 JQuery部分をStimulusに置き換えた実装に変更していきます。
スレッドの購読やいいねも是非お願いします。
ここまで読んでいただきありがとうございました。