6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rails + Vue.jsで都道府県に応じた市区町村を表示するセレクトボックスを作成する

Last updated at Posted at 2021-08-01

はじめに

表題のようなセレクトボックスを作成する機会があったため備忘録として記事を残しておきます。

完成品

本記事ではrails newから始め、以下のようなセレクトボックスを作成するところまで手順を書いていきます。
ezgif-3-c6816a181c48.gif

環境

  • Ruby 2.7.3
  • Rails 6.0.4
  • Vue 2.6.14

1. 都道府県, 市区町村モデル作成

以下のER図のような都道府県(Prefecture), 市区町村(City)モデルを作成していきます。
ERD

まずは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制約を付けておきます。

db/migrate/20210731145941_create_prefectures.rb
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カラムのバリデーションを追記します。

app/models/prefecture.rb
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

マイグレーションファイルを以下のように編集しましょう。
市区町村は名称の重複があるためユニーク制約は付けないようにします。

db/migrate/20210731150527_create_cities.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カラムのバリデーションを追記します。

app/models/city.rb
class City < ApplicationRecord
  belongs_to :prefecture

  validates :name, presence: true
end

また都道府県(Prefecture)モデルに市区町村(City)とのアソシエーションを追記します。

app/models/prefecture.rb
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列目に市区町村名...といったようになっています。

lib/自治体一覧表.csv
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を書いていきましょう。以下のようになります。

db/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

ルーティングを設定しておきます。

config/routes.rb
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コンポーネントを読み込むためのコードを追記します。

app/views/home/index.html.erb
<%= javascript_pack_tag 'hello_vue' %>

それではrails sコマンドを実行しルートパスにアクセスしてみましょう。以下のように"Hello Vue!"という文字が表示されていればVue.jsが動作しています。
root_1

4. セレクトボックスに都道府県を表示する

ビューに以下を追記してフォームを表示します。

app/views/home/index.html.erb
<%= form_with id: 'example_form' do |f| %>
  <%= f.select :prefecture, [] %>
  <%= f.select :city, [] %>
  <%= f.submit '検索' %>
<% end %>

<%= javascript_pack_tag 'hello_vue' %>

root_2

今回はVueコンポーネントを使用せずにerb上のタグにVueを適応させていこうと思います。app/javascript/app.vueは削除しapp/javascript/packs/hello_vue.jsを以下のように編集します。

app/javascript/packs/hello_vue.js
import Vue from 'vue/dist/vue.esm';

new Vue({
  el: '#example_form',
  data: {
    message: 'Hello Vue'
  }
});

上記ではdatamessageという変数を宣言しています。erb内でこの変数が読み込めるかテストしてみましょう。

app/views/home/index.html.erb
<%= form_with id: 'example_form' do |f| %>
  <%= f.select :prefecture, [] %>
  <%= f.select :city, [] %>
  <%= f.submit '検索' %>
  {{message}}
<% end %>

<%= javascript_pack_tag 'hello_vue' %>

root_3
読み込めていますね!確認が出来たらmessage変数は削除しておいてください。
それではセレクトボックスに都道府県を表示していきましょう。

都道府県のデータはデータベース上に登録されているためRails上で読み込みVue側に渡す必要があります。
RailsからVueにデータを渡す方法はいくつかありますが今回はgonというgemを使用します。
Gemfileに以下を追記しbundle installして下さい。
gonについては以下の記事が参考になります。
gonを使ったRailsとJavascriptの連携について

Gemfile
gem 'gon'

gonを使用するためにapplication.html.erbに以下を追記します。

app/views/layouts/application.html.erb
...
    <%= include_gon %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
...

Prefectureモデルの中でセレクトボックスで使用する情報はidnameになります。(idは都道府県名を表示するだけなら必須ではありませんがセレクトボックスの使用用途を考えると後々必要になると思います)
というわけでindexアクション内でidnameの情報をJSONデータに変換しVue側に渡します。

app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    gon.prefectures = Prefecture.all.to_json only: %i[id name]
  end
end

Rails側でJSONに変換して格納した変数をJavaScript側でJSON.parseメソッドを用いてJSONからオブジェクトへ変換します。

app/javascript/packs/hello_vue.js
import Vue from 'vue/dist/vue.esm';

new Vue({
  el: '#example_form',
  data: {
    prefectures: JSON.parse(gon.prefectures)
  }
});

上記のイメージがわかない場合はブラウザの開発者ツールのコンソールでコードを打ってみるといいかもです。
console
以上でVue側に都道府県のデータを渡せました。
あとはerb側でVueのコードを記述するだけです。v-forを使用し配列prefecturesの繰り返し処理を行います。

app/views/home/index.html.erb
<%= 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' %>

以上でセレクトボックスに都道府県を表示することが出来ました。

root_4
↑ 省略してますが沖縄県までちゃんと表示されています!

ちなみに5章の実装を行わないのであればこんな面倒なことをしなくても以下のコードで同様の実装が出来ます。

<%= f.select :pref, options_for_select(Prefecture.all.map{ |pref| [pref.name, pref.id] }) %>

5. セレクトボックスに都道府県に応じた市区町村を表示する

さて、いよいよ本題の実装に入ります。
セレクトボックスで都道府県が選択されたらVueがAjax通信を行いRails側から市区町村データを受け取る機能を実装していきます。
まずRails側で市区町村データを返す機能を実装します。

$ rails g controller cities
app/controllers/cities_controller.rb
class CitiesController < ApplicationController
  def set_cities
    cities = Prefecture.find(params[:id]).cities
    render json: cities.all.to_json(only: %i[id name])
  end
end
config/routes.rb
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()も定義します。

app/javascript/packs/hello_vue.js
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で指定されたメソッドが実行されるようになります。

app/views/home/index.html.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, [] %>
  <%= f.submit '検索' %>
<% end %>

<%= javascript_pack_tag 'hello_vue' %>

それではブラウザ上で動作確認してみましょう。セレクトボックスで適当な都道府県を選択すると開発者ツールのコンソール上に対応するidが表示されると思います。
root_5
getCities()メソッドに都道府県のidが渡されることが確認出来ました。
次にAjax通信の実装をしていきます。

VueでAjax通信をするにはaxiosというライブラリを読み込む必要があります。
またAjax通信する際にCSRFトークンを作成しておかないと422エラーを吐かれます。
詳しくはこちらの記事を参考にしてみてください。

app/javascript/packs/hello_vue.js
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を編集して市区町村名を表示するよう実装します。

app/views/home/index.html.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' %>

ブラウザ上で確認してみましょう。
無事、セレクトボックスに都道府県に応じた市区町村を表示できました!
cities
cities

ですが1つだけ問題が発生しています。
北海道を選択した際コンソールを見るとエラーが発生しています。
error
北海道には「泊村」が2つあるためダブってるよとエラーが出ているっぽいです。
このエラーの対応は人によって異なると思いますが手っ取り早くオブジェクトの名称を「泊村(国後郡)」のように変更しちゃえばいいのかなと思います。

参考

6
5
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
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?