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

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

# 完成品
本記事では`rails　new`から始め、以下のようなセレクトボックスを作成するところまで手順を書いていきます。
![ezgif-3-c6816a181c48.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/bea4364e-81ff-35bc-0ae4-1269b4e5fda1.gif)

# 環境
- Ruby 2.7.3
- Rails 6.0.4
- Vue 2.6.14

# 1. 都道府県, 市区町村モデル作成
以下のER図のような都道府県(Prefecture), 市区町村(City)モデルを作成していきます。
![ERD](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/e0111266-7ef3-7b37-d81f-3984251bb938.png)

まずは`rails　new`でRailsアプリケーションを新規作成します。

```bash
$ rails _6.0.4_ new example_app
$ cd example_app
$ rails db:create
```

都道府県(Prefecture)モデルを作成していきます。

```bash
$ 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制約を付けておきます。

```ruby: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に適応させます。

```bash
$ 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カラムのバリデーションを追記します。

```ruby:app/models/prefecture.rb
class Prefecture < ApplicationRecord
  validates :name, presence: true, uniqueness: { case_sensitive: false }
end
```

次に市区町村(City)モデルを作成します。

```bash
$ 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
```

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

```ruby: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に適応させます。

```bash
$ rails db:migrate
== 20210731150527 CreateCities: migrating =====================================
-- create_table(:cities)
   -> 0.0017s
== 20210731150527 CreateCities: migrated (0.0018s) ============================
```

モデルファイルにnameカラムのバリデーションを追記します。

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

  validates :name, presence: true
end
```

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

```ruby:app/models/prefecture.rb
class Prefecture < ApplicationRecord
  has_many :cities

  validates :name, presence: true, uniqueness: { case_sensitive: false }
end
```

# 2. 都道府県, 市区町村オブジェクトを作成する

[総務省のサイト](https://www.soumu.go.jp/denshijiti/code.html)から都道府県, 市区町村のリストデータ(Excelファイル)をダウンロードしましょう。
ダウンロードしたらExcelやNumbersで開きCSVファイルとして書き出しを行います。
書き出したCSVファイルをRailsアプリケーションの`lib/`フォルダに入れましょう。
このCSVファイルをRubyで読み込み、都道府県のデータと市区町村のデータをうまく配列に変換していきます。

CSVファイルを開いてみると以下のようになっていると思います。
1列目に自治体コード, 2列目に都道府県名, 3列目に市区町村名...といったようになっています。

```lib/自治体一覧表.csv
010006,北海道,,ﾎｯｶｲﾄﾞｳ,,,
011002,北海道,札幌市,ﾎｯｶｲﾄﾞｳ,ｻｯﾎﾟﾛｼ,,
012025,北海道,函館市,ﾎｯｶｲﾄﾞｳ,ﾊｺﾀﾞﾃｼ,,
012033,北海道,小樽市,ﾎｯｶｲﾄﾞｳ,ｵﾀﾙｼ,,
012041,北海道,旭川市,ﾎｯｶｲﾄﾞｳ,ｱｻﾋｶﾜｼ,,
...
```

このデータを使い都道府県の配列を作っていきます。

```ruby
require 'csv'

# CSV読み込み
file_path = 'lib/自治体一覧表.csv'

# CSVを1行毎の配列へ変換
csv_data = CSV.read(file_path)
# => [["010006", "北海道", nil, "ﾎｯｶｲﾄﾞｳ", nil, nil, nil], ["011002", "北海道", "札幌市", ...
```

`csv_data`を以下のように処理すると2列目の都道府県名のみの配列になります。

```ruby
# 2列目のみの配列へ変換
csv_data.map { |row| row[1] }
# => ["北海道", "北海道", "北海道", "北海道", ... "沖縄県", "沖縄県", "沖縄県"]

# uniqメソッドを使用することで重複を削除できます
prefectures_list = csv_data.map { |row| row[1] }.uniq
# => ["北海道", "青森県", "岩手県", ...  "宮崎県", "鹿児島県", "沖縄県"]
```

次に市区町村の配列を作っていきます。
市区町村オブジェクトを作成する際、都道府県名も必要となってくるため以下のような配列を作ることを目標としていきます。

```ruby
[["北海道", "札幌市"], ["北海道", "函館市"],  ... ["沖縄県", "竹富町"], ["沖縄県", "与那国町"]]
```

都道府県の配列を作ったコードに習うと以下のようなコードになると思いますが元データの都合上nilの入ったデータが出来てしまいます。

```ruby
# 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を書いていきましょう。以下のようになります。

```ruby: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
```

さて、初期データを作成してみましょう！

```bash
$ rails db:seed
```

Railsコンソールでデータが作成出来ているか確認してみます。

```bash
$ 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 }
千代田区
中央区
港区
...
八丈町
青ヶ島村
小笠原村
```

都道府県, 市区町村の数を確認しましたが合っています。
※ 市町村の数は[総務省によると](https://www.soumu.go.jp/kouiki/kouiki.html)1,724あるそうです。これに東京都の特別区23を足すことで上記の1,747になります。
関連付けもしっかり出来ています。
20行程度のコードで都道府県, 市区町村のデータをまとめられるRuby素敵です。

以上で必要なオブジェクトは揃いました。これから表題の内容に入っていきます。

# 3. Vueの導入

RailsアプリケーションにVue.jsを導入していきましょう。
本章ではVueコンポーネントをerbから読み込んでHello Worldを表示していきたいと思います。

ルートページ用の適当なコントローラを作っておきます。

```bash
$ rails g controller home index
```

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

```ruby:config/routes.rb
Rails.application.routes.draw do
  root 'home#index'
end
```

以下のコマンドを叩いてRailsアプリケーションにVue.jsを導入します。
`app/javascript`ディレクトリ下にVueコンポーネントとVueコンポーネントを読み込むjsファイルが作成されました。

```bash
$ rails webpacker:install:vue
...
      create  app/javascript/packs/hello_vue.js
      create  app/javascript/app.vue
...
Webpacker now supports Vue.js 🎉
```

以降は`bin/webpack`を実行してファイルの変更がある度に自動でコンパイルしておくようにします。

```bash
$ bin/webpack-dev-server
```

`Home#index`ビューに作成されたVueコンポーネントを読み込むためのコードを追記します。

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

それでは`rails s`コマンドを実行しルートパスにアクセスしてみましょう。以下のように"Hello Vue!"という文字が表示されていればVue.jｓが動作しています。
![root_1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/e4468609-72cd-56e6-aa38-b9692c5b9f79.png)

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

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

```erb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/ed02415a-3f5a-0433-f0d8-5e58e6bbb19a.png)

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

```javascript: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内でこの変数が読み込めるかテストしてみましょう。

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/0857d30a-0ffa-aba0-e7b4-feeff97b1faf.png)
読み込めていますね！確認が出来たら`message`変数は削除しておいてください。
それではセレクトボックスに都道府県を表示していきましょう。

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

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

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


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

```ruby: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からオブジェクトへ変換します。

```javascript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/60b0051e-dd2c-2d32-6fe5-c5dc8593a3e1.png)
以上でVue側に都道府県のデータを渡せました。
あとはerb側でVueのコードを記述するだけです。`v-for`を使用し配列`prefectures`の繰り返し処理を行います。

```erb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/f8f714c0-0e13-fe01-d24a-5bc55af99565.png)
↑ 省略してますが沖縄県までちゃんと表示されています！

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

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

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

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

```bash
$ rails g controller cities
```

```ruby: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
```

```ruby: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()`も定義します。

```javascript: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`で指定されたメソッドが実行されるようになります。

```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, [] %>
  <%= f.submit '検索' %>
<% end %>

<%= javascript_pack_tag 'hello_vue' %>
```

それではブラウザ上で動作確認してみましょう。セレクトボックスで適当な都道府県を選択すると開発者ツールのコンソール上に対応する`id`が表示されると思います。
![root_5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/d379e078-f6be-3174-22ca-0a22d05abada.png)
`getCities()`メソッドに都道府県の`id`が渡されることが確認出来ました。
次にAjax通信の実装をしていきます。

VueでAjax通信をするには`axios`というライブラリを読み込む必要があります。
またAjax通信する際にCSRFトークンを作成しておかないと422エラーを吐かれます。
詳しくは[こちらの記事](https://qiita.com/ngron/items/2faae8068baa093d6aba)を参考にしてみてください。

```javascript: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を編集して市区町村名を表示するよう実装します。

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/34707112-5065-ec68-9aaa-36beabc42fe8.png)
![cities](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/f558e992-86ab-75d9-0fb1-0441d1cd4651.png)

ですが1つだけ問題が発生しています。
北海道を選択した際コンソールを見るとエラーが発生しています。
![error](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779971/7a82a40a-f85e-4206-a41f-67ac97a2bf55.png)
北海道には「泊村」が2つあるためダブってるよとエラーが出ているっぽいです。
このエラーの対応は人によって異なると思いますが手っ取り早くオブジェクトの名称を「泊村(国後郡)」のように変更しちゃえばいいのかなと思います。

# 参考
- https://www.soumu.go.jp/denshijiti/code.html
- https://www.soumu.go.jp/kouiki/kouiki.html
- https://qiita.com/s_nakamura/items/5d153f7d9db1b1190296
- https://qiita.com/ngron/items/2faae8068baa093d6aba