はじめに
表題のようなセレクトボックスを作成する機会があったため備忘録として記事を残しておきます。
完成品
本記事ではrails new
から始め、以下のようなセレクトボックスを作成するところまで手順を書いていきます。
環境
- Ruby 2.7.3
- Rails 6.0.4
- Vue 2.6.14
1. 都道府県, 市区町村モデル作成
以下のER図のような都道府県(Prefecture), 市区町村(City)モデルを作成していきます。
まずはrails new
でRailsアプリケーションを新規作成します。
$ rails _6.0.4_ new example_app
$ cd example_app
$ rails db:create
都道府県(Prefecture)モデルを作成していきます。
$ rails g model Prefecture name:string
Running via Spring preloader in process 71121
invoke active_record
create db/migrate/20210731145941_create_prefectures.rb
create app/models/prefecture.rb
マイグレーションファイルを以下のように編集しましょう。
都道府県名は一意なのでunique制約を付けておきます。
class CreatePrefectures < ActiveRecord::Migration[6.0]
def change
create_table :prefectures do |t|
t.string :name, null: false
t.timestamps
end
add_index :prefectures, :name, unique: true
end
end
マイグレーションをDBに適応させます。
$ rails db:migrate
== 20210731145941 CreatePrefectures: migrating ================================
-- create_table(:prefectures)
-> 0.0010s
-- add_index(:prefectures, :name, {:unique=>true})
-> 0.0004s
== 20210731145941 CreatePrefectures: migrated (0.0016s) =======================
モデルファイルにnameカラムのバリデーションを追記します。
class Prefecture < ApplicationRecord
validates :name, presence: true, uniqueness: { case_sensitive: false }
end
次に市区町村(City)モデルを作成します。
$ rails g model City name:string prefecture:references
Running via Spring preloader in process 71410
invoke active_record
create db/migrate/20210731150527_create_cities.rb
create app/models/city.rb
マイグレーションファイルを以下のように編集しましょう。
市区町村は名称の重複があるためユニーク制約は付けないようにします。
class CreateCities < ActiveRecord::Migration[6.0]
def change
create_table :cities do |t|
t.string :name, null: false
t.references :prefecture, null: false, foreign_key: true
t.timestamps
end
end
end
マイグレーションをDBに適応させます。
$ rails db:migrate
== 20210731150527 CreateCities: migrating =====================================
-- create_table(:cities)
-> 0.0017s
== 20210731150527 CreateCities: migrated (0.0018s) ============================
モデルファイルにnameカラムのバリデーションを追記します。
class City < ApplicationRecord
belongs_to :prefecture
validates :name, presence: true
end
また都道府県(Prefecture)モデルに市区町村(City)とのアソシエーションを追記します。
class Prefecture < ApplicationRecord
has_many :cities
validates :name, presence: true, uniqueness: { case_sensitive: false }
end
2. 都道府県, 市区町村オブジェクトを作成する
総務省のサイトから都道府県, 市区町村のリストデータ(Excelファイル)をダウンロードしましょう。
ダウンロードしたらExcelやNumbersで開きCSVファイルとして書き出しを行います。
書き出したCSVファイルをRailsアプリケーションのlib/
フォルダに入れましょう。
このCSVファイルをRubyで読み込み、都道府県のデータと市区町村のデータをうまく配列に変換していきます。
CSVファイルを開いてみると以下のようになっていると思います。
1列目に自治体コード, 2列目に都道府県名, 3列目に市区町村名...といったようになっています。
010006,北海道,,ホッカイドウ,,,
011002,北海道,札幌市,ホッカイドウ,サッポロシ,,
012025,北海道,函館市,ホッカイドウ,ハコダテシ,,
012033,北海道,小樽市,ホッカイドウ,オタルシ,,
012041,北海道,旭川市,ホッカイドウ,アサヒカワシ,,
...
このデータを使い都道府県の配列を作っていきます。
require 'csv'
# CSV読み込み
file_path = 'lib/自治体一覧表.csv'
# CSVを1行毎の配列へ変換
csv_data = CSV.read(file_path)
# => [["010006", "北海道", nil, "ホッカイドウ", nil, nil, nil], ["011002", "北海道", "札幌市", ...
csv_data
を以下のように処理すると2列目の都道府県名のみの配列になります。
# 2列目のみの配列へ変換
csv_data.map { |row| row[1] }
# => ["北海道", "北海道", "北海道", "北海道", ... "沖縄県", "沖縄県", "沖縄県"]
# uniqメソッドを使用することで重複を削除できます
prefectures_list = csv_data.map { |row| row[1] }.uniq
# => ["北海道", "青森県", "岩手県", ... "宮崎県", "鹿児島県", "沖縄県"]
次に市区町村の配列を作っていきます。
市区町村オブジェクトを作成する際、都道府県名も必要となってくるため以下のような配列を作ることを目標としていきます。
[["北海道", "札幌市"], ["北海道", "函館市"], ... ["沖縄県", "竹富町"], ["沖縄県", "与那国町"]]
都道府県の配列を作ったコードに習うと以下のようなコードになると思いますが元データの都合上nilの入ったデータが出来てしまいます。
# 2, 3列目のみの配列へ変換
csv_data.map { |row| row[1, 2] }
# => [["北海道", nil], ["北海道", "札幌市"], ... ["沖縄県", "竹富町"], ["沖縄県", "与那国町"]]
# 3列目がnilだった場合処理を飛ばす
csv_data.map do |row|
next if row[2] == nil
row[1, 2]
end
# => [nil, ["北海道", "札幌市"], ... ["沖縄県", "竹富町"], ["沖縄県", "与那国町"]]
# compactメソッドで配列内のnilを削除
cities_list = csv_data.map do |row|
next if row[2] == nil
row[1, 2]
end.compact
# => [["北海道", "札幌市"], ["北海道", "函館市"], ... ["沖縄県", "竹富町"], ["沖縄県", "与那国町"]]
長くなりましたが以上でCSVデータから都道府県, 市区町村の配列が作成出来ました。上記のコードを用いてPrefectureオブジェクト, Cityオブジェクトを作成するseeds.rbを書いていきましょう。以下のようになります。
require 'csv'
# CSV読み込み
file_path = 'lib/自治体一覧表.csv'
csv_data = CSV.read(file_path)
# 都道府県データ抽出
prefectures_list = csv_data.map { |row| row[1] }.uniq
# 市区町村データ抽出
cities_list = csv_data.map do |row|
next if row[2] == nil
row[1, 2]
end.compact
# 都道府県データ作成
prefectures_list.each do |prefecture|
Prefecture.create!(name: prefecture)
end
# 市区町村データ作成
cities_list.each do |prefecture, city|
prefecture = Prefecture.find_by(name: prefecture)
prefecture.cities.create(name: city)
end
さて、初期データを作成してみましょう!
$ rails db:seed
Railsコンソールでデータが作成出来ているか確認してみます。
$ rails c
> Prefecture.count
=> 47
> City.count
=> 1747
> Prefecture.all.each { |pref| puts pref.name }
北海道
青森県
岩手県
...
宮崎県
鹿児島県
沖縄県
> Prefecture.find_by(name: '東京都').cities.each { |city| puts city.name }
千代田区
中央区
港区
...
八丈町
青ヶ島村
小笠原村
都道府県, 市区町村の数を確認しましたが合っています。
※ 市町村の数は総務省によると1,724あるそうです。これに東京都の特別区23を足すことで上記の1,747になります。
関連付けもしっかり出来ています。
20行程度のコードで都道府県, 市区町村のデータをまとめられるRuby素敵です。
以上で必要なオブジェクトは揃いました。これから表題の内容に入っていきます。
3. Vueの導入
RailsアプリケーションにVue.jsを導入していきましょう。
本章ではVueコンポーネントをerbから読み込んでHello Worldを表示していきたいと思います。
ルートページ用の適当なコントローラを作っておきます。
$ rails g controller home index
ルーティングを設定しておきます。
Rails.application.routes.draw do
root 'home#index'
end
以下のコマンドを叩いてRailsアプリケーションにVue.jsを導入します。
app/javascript
ディレクトリ下にVueコンポーネントとVueコンポーネントを読み込むjsファイルが作成されました。
$ rails webpacker:install:vue
...
create app/javascript/packs/hello_vue.js
create app/javascript/app.vue
...
Webpacker now supports Vue.js 🎉
以降はbin/webpack
を実行してファイルの変更がある度に自動でコンパイルしておくようにします。
$ bin/webpack-dev-server
Home#index
ビューに作成されたVueコンポーネントを読み込むためのコードを追記します。
<%= javascript_pack_tag 'hello_vue' %>
それではrails s
コマンドを実行しルートパスにアクセスしてみましょう。以下のように"Hello Vue!"という文字が表示されていればVue.jsが動作しています。
4. セレクトボックスに都道府県を表示する
ビューに以下を追記してフォームを表示します。
<%= form_with id: 'example_form' do |f| %>
<%= f.select :prefecture, [] %>
<%= f.select :city, [] %>
<%= f.submit '検索' %>
<% end %>
<%= javascript_pack_tag 'hello_vue' %>
今回はVueコンポーネントを使用せずにerb上のタグにVueを適応させていこうと思います。app/javascript/app.vue
は削除しapp/javascript/packs/hello_vue.js
を以下のように編集します。
import Vue from 'vue/dist/vue.esm';
new Vue({
el: '#example_form',
data: {
message: 'Hello Vue'
}
});
上記ではdata
にmessage
という変数を宣言しています。erb内でこの変数が読み込めるかテストしてみましょう。
<%= form_with id: 'example_form' do |f| %>
<%= f.select :prefecture, [] %>
<%= f.select :city, [] %>
<%= f.submit '検索' %>
{{message}}
<% end %>
<%= javascript_pack_tag 'hello_vue' %>
読み込めていますね!確認が出来たらmessage
変数は削除しておいてください。
それではセレクトボックスに都道府県を表示していきましょう。
都道府県のデータはデータベース上に登録されているためRails上で読み込みVue側に渡す必要があります。
RailsからVueにデータを渡す方法はいくつかありますが今回はgon
というgemを使用します。
Gemfileに以下を追記しbundle install
して下さい。
gon
については以下の記事が参考になります。
gonを使ったRailsとJavascriptの連携について
gem 'gon'
gon
を使用するためにapplication.html.erb
に以下を追記します。
...
<%= include_gon %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
...
Prefectureモデルの中でセレクトボックスで使用する情報はid
とname
になります。(id
は都道府県名を表示するだけなら必須ではありませんがセレクトボックスの使用用途を考えると後々必要になると思います)
というわけでindexアクション内でid
とname
の情報をJSONデータに変換しVue側に渡します。
class HomeController < ApplicationController
def index
gon.prefectures = Prefecture.all.to_json only: %i[id name]
end
end
Rails側でJSONに変換して格納した変数をJavaScript側でJSON.parseメソッドを用いてJSONからオブジェクトへ変換します。
import Vue from 'vue/dist/vue.esm';
new Vue({
el: '#example_form',
data: {
prefectures: JSON.parse(gon.prefectures)
}
});
上記のイメージがわかない場合はブラウザの開発者ツールのコンソールでコードを打ってみるといいかもです。
以上でVue側に都道府県のデータを渡せました。
あとはerb側でVueのコードを記述するだけです。v-for
を使用し配列prefectures
の繰り返し処理を行います。
<%= form_with id: 'example_form' do |f| %>
<%= f.select :prefecture do %>
<option v-for="prefecture in prefectures" :key="prefecture.name" :value="prefecture.id">
{{prefecture.name}}
</option>
<% end %>
<%= f.select :city, [] %>
<%= f.submit '検索' %>
<% end %>
<%= javascript_pack_tag 'hello_vue' %>
以上でセレクトボックスに都道府県を表示することが出来ました。
ちなみに5章の実装を行わないのであればこんな面倒なことをしなくても以下のコードで同様の実装が出来ます。
<%= f.select :pref, options_for_select(Prefecture.all.map{ |pref| [pref.name, pref.id] }) %>
5. セレクトボックスに都道府県に応じた市区町村を表示する
さて、いよいよ本題の実装に入ります。
セレクトボックスで都道府県が選択されたらVueがAjax通信を行いRails側から市区町村データを受け取る機能を実装していきます。
まずRails側で市区町村データを返す機能を実装します。
$ rails g controller cities
class CitiesController < ApplicationController
def set_cities
cities = Prefecture.find(params[:id]).cities
render json: cities.all.to_json(only: %i[id name])
end
end
Rails.application.routes.draw do
root 'home#index'
post 'set_cities', to: 'cities#set_cities'
end
これで/set_cities
に都道府県のid
を渡せば対応した市区町村のJSONデータが帰ってくるようになりました。
Vue側の実装をしていきます。
まずセレクトボックスで選択された都道府県のid
を取得していきたいと思います。
Vueに選択された都道府県のid
を格納する変数selectedPref
を定義します。また、都道府県が選択された際実行するメソッドgetCities()
も定義します。
import Vue from 'vue/dist/vue.esm';
new Vue({
el: '#example_form',
data: {
selectedPref: '',
prefectures: JSON.parse(gon.prefectures)
},
methods: {
getCities: function(prefecture) {
console.log(prefecture); // とりあえず取得したデータをコンソール上で表示
}
}
});
erbを編集しselect
タグにv-model
と@change
を定義します。
セレクトボックスで選択された内容がv-model
に指定された変数に格納されるようになります。
また、セレクトボックスで選択された内容が変更された場合@change
で指定されたメソッドが実行されるようになります。
<%= form_with id: 'example_form' do |f| %>
<%= f.select :prefecture, nil, {}, {'v-model': 'selectedPref', '@change': 'getCities(selectedPref)'} do %>
<option v-for="prefecture in prefectures" :key="prefecture.name" :value="prefecture.id">{{prefecture.name}}</option>
<% end %>
<%= f.select :city, [] %>
<%= f.submit '検索' %>
<% end %>
<%= javascript_pack_tag 'hello_vue' %>
それではブラウザ上で動作確認してみましょう。セレクトボックスで適当な都道府県を選択すると開発者ツールのコンソール上に対応するid
が表示されると思います。
getCities()
メソッドに都道府県のid
が渡されることが確認出来ました。
次にAjax通信の実装をしていきます。
VueでAjax通信をするにはaxios
というライブラリを読み込む必要があります。
またAjax通信する際にCSRFトークンを作成しておかないと422エラーを吐かれます。
詳しくはこちらの記事を参考にしてみてください。
import Vue from 'vue/dist/vue.esm';
import axios from 'axios';
// CSRFトークン作成
axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN' : document.querySelector('meta[name="csrf-token"]').getAttribute('content')
};
new Vue({
el: '#example_form',
data: {
selectedPref: '',
cities: [], // 取得した市区町村データを格納する変数
prefectures: JSON.parse(gon.prefectures)
},
methods: {
getCities: function(prefecture) {
// Ajax通信
axios
// /set_citiesへPOSTリクエストを送信
.post('/set_cities', {
id: prefecture
})
// Rails側から帰ってきたデータをcitiesへ格納
.then((response) => {
this.cities = response.data
})
}
}
});
erbを編集して市区町村名を表示するよう実装します。
<%= form_with id: 'example_form' do |f| %>
<%= f.select :prefecture, nil, {}, {'v-model': 'selectedPref', '@change': 'getCities(selectedPref)'} do %>
<option v-for="prefecture in prefectures" :key="prefecture.name" :value="prefecture.id">{{prefecture.name}}</option>
<% end %>
<%= f.select :city do %>
<option v-for="city in cities" :key="city.name" :value="city.id">{{city.name}}</option>
<% end %>
<%= f.submit '検索' %>
<% end %>
<%= javascript_pack_tag 'hello_vue' %>
ブラウザ上で確認してみましょう。
無事、セレクトボックスに都道府県に応じた市区町村を表示できました!
ですが1つだけ問題が発生しています。
北海道を選択した際コンソールを見るとエラーが発生しています。
北海道には「泊村」が2つあるためダブってるよとエラーが出ているっぽいです。
このエラーの対応は人によって異なると思いますが手っ取り早くオブジェクトの名称を「泊村(国後郡)」のように変更しちゃえばいいのかなと思います。