Rails6でDBと連動する動的な多段セレクトボックスの作り方【Ajax不使用】

## 使用言語・前提
- Ruby(Ruby on Rails 6.0)
- JavaScript(jQuery)
- formではbootstrapformを利用。

## 実現したいDBと連携する動的な多段セレクトボックスとは？
大、中、小のカテゴリのモデルが存在していて、これからのモデルはリレーションされているとします。

``` large_category.rb
class LargeCategory < ApplicationRecord
  has_many :medium_categories, dependent: :restrict_with_error
end
```

``` medium_category.rb
class MediumCategory < ApplicationRecord
  belongs_to :large_category
  has_many :small_categories, dependent: :destroy
end
```
``` small_category.rb
class SmallCategory < ApplicationRecord
  belongs_to :medium_category
end
```

・大カテゴリのセレクトボックスの選択した内容により、中カテゴリの選択内容が変化。
・そして中カテゴリの選択内容により、小カテゴリの選択内容が変化するイメージです。

イメージとしては[@jnchito](https://qiita.com/jnchito)さんが書いている記事が、動画になっているのでわかりやすいです。
[https://qiita.com/jnchito/items/59a5f6bea3d7be84b839](https://qiita.com/jnchito/items/59a5f6bea3d7be84b839)

色々と実装するやり方はあると思いますが、今回は[@i__kobe](https://qiita.com/i__kobe)さんの方法参考に実装しました。
[https://qiita.com/i__kobe/items/3974d9ec78dbf082a5ef](https://qiita.com/i__kobe/items/3974d9ec78dbf082a5ef)

ただし今回、大中小と三段になっていることや、bootstrapformを使用していて、labelが自動生成させることから、そのままでは実装ができなかったため、改良を入れています。

### 全体の大まかな流れ
コードだけの話をしても、わかりづらいと思いますので、全体の大まかな流れを説明していきます。（newの状態を想定しています）

Javascriptを使って、選択時に該当するセレクトボックスを読み込むというを繰り返していきます。

#### 事前準備
tamplateという読み込み時は描画されず、javascriptによって表示させるHTML仕組みを利用します。
tamplateで予め、中、小カテゴリの選択肢をすべてコーディングしておきます。

#### ページ読み込み時
- 大カテゴリ（未選択）
- 中カテゴリ（ダミー）
- 小カテゴリ（ダミー）
最初は、中、小カテゴリともにダミーを表示させておきます。

###　大カテゴリ選択時
- 大カテゴリ（選択済）
- 中カテゴリ（未選択）　←ダミーと該当するtamplateを入れ替える。
- 小カテゴリ（ダミー）

大カテゴリを選択すると、中カテゴリのダミーを削除して、大カテゴリに紐づいた中カテゴリのセレクトボックスを出現させます。

###　中カテゴリ選択時
- 大カテゴリ（選択済）
- 中カテゴリ（選択済）　
- 小カテゴリ（未選択）　←ダミーと該当するtamplateを入れ替える。

中カテゴリを選択すると、小カテゴリのダミーを削除して、中カテゴリに紐づいた小カテゴリのセレクトボックスを出現させます。
 
###　大カテゴリを未選択に戻す
- 大カテゴリ（未選択）
- 中カテゴリ（ダミー）　←ダミーに戻す
- 小カテゴリ（ダミー）　←ダミーに戻す

大カテゴリを未選択に戻すと、選択された中、小カテゴリを削除して、ダミーを復活せます。

とう流れを、選択している状態で行うだけです。

### セレクトボックスを実装する
まずは、viewの設定からしておきます。
今回はeditもほぼ同じなので、new、editで共通化できるようにパーシャルで作成しています。


```erb:_form.html.erb
<div class="form_panel">
  <%= bootstrap_form_for(@post) do |f| %>

    <div class="field">
      <%= f.collection_select :large_category_id, @large_categories, :id, :name, {include_blank: "大カテゴリを選択(必須)", prompt: false}%>
    </div>

<!-- 中カテゴリが差し込ませるポイントを指定するために追加　※１ -->
    <div id="medium_category_insert_point"></div> 


<!-- -----------EDIT時、中カテゴリーのダミー表示の切替 ※２---------------------- -->
    <div id="medium_category">
      <div class="field ">
        <% if @medium_categories.blank? %>
              <%= f.select :medium_category_id, [], include_blank: "---"%>
        <% else %>
          <%= f.collection_select :medium_category_id, @medium_categories, :id, :name, {include_blank: "---", prompt: false } %>
        <% end %>
      </div>
    </div>


<!-- 小カテゴリが差し込ませるポイントを指定するために追加 -->
    <div id="small_category_insert_point"></div>


<!-- -----------EDIT時、小カテゴリのダミー表示の切替---------------------- -->
    <div id="small_category">
      <div class="field ">
        <% if @small_categories.blank?  %>
          <%= f.select :small_category_id, [], include_blank: "---"%>
          <% else %>
            <%= f.collection_select :small_category_id, @small_categories, :id, :name, {include_blank: "---", prompt: false } %>
        <% end %>
      </div>
    </div>

<!-- -----------中カテゴリのtamplateを作成 ※3---------------------- -->
    <% @large_categories.each do |large_category|  %>
      <template id="medium_category_<%= large_category.id %>"><!-- このidをもとに呼び出される-->
      <div id="selected_medium_category">
        <div class="field" >
        <%= f.collection_select :medium_category_id, large_category.medium_categories, :id, :name, {include_blank: "---", prompt: false} %>
        </div>
      </div>
      </template>

<!-- -----------中カテゴリのtamplate作成途中に、小カテゴリのtamplateを作成を挟む※4---------------------- -->
      <% large_category.medium_categories.each do |medium_category| %>
        <template id="small_category_<%= medium_category.id %>"><!-- このidをもとに呼び出される-->
        <div id="selected_small_category">
          <div class="field" >
          <%= f.collection_select :small_category_id, medium_category.small_categories, :id, :name, {include_blank: "---", prompt: false} %>
          </div>
        </div>
        </template>

      <% end %>
    <% end %>

〜〜ここから省略〜〜〜

<% end %><!-- bootstrap_form_for -->

</div>
<!-- javascriptの読み込み -->
<%= javascript_pack_tag 'category_select_box', 'data-turbolinks-track': 'reload'  %>

```

- ※１ ダミーやテンプレートを差し込む位置を指定するために追加しています。
- ※2 edit時はカテゴリが選択されているので、デフォルトで表示できるようにしています。
- ※３ リスト分を繰り返し処理で、中カテゴリのtamplateを作成していきます
- ※４ 中カテゴリが１つのtamplate作る事に、中カテゴリに紐づく小カテゴリのtamplateを作成しています。


### コントローラー（念のため）

``` posts_controller.rb
#userとpostがリレーションされています。
  def new
    @large_categories = current_user.large_categories #ユーザーに紐づく大カテゴリを取得
    @post = current_user.posts.new() #インスタンスを作成
  end
end
```


### Javascriptの処理（jQuery）

``` app/javascript/packs/category_select.js
$(document).on('turbolinks:load', function() {

//復活させるダミーの中カテゴリのセレクトボックス
  let defaultMediumCategorySelect = `<div id="medium_category"><div class="field"><div class="form-group"><label for="post_large_category_id">中カテゴリ</label><select name="medium_category", class="form-control">
  <option value>---</option>
  </select></div></div></div>`;

 //復活させるダミーの小カテゴリのセレクトボックス
  let defaultSmallCategorySelect = `<div id="small_category"><div class="field"><div class="form-group"><label for="post_large_category_id">小カテゴリ</label><select name="medium_category", class="form-control">
  <option value>---</option>
  </select></div></div></div>`;

//中カテゴリの処理
 $(document).on('change', '#post_large_category_id', function() {
  let categoryVal = $('#post_large_category_id').val();

  //大カテゴリが変更されてvalueに値が入った場合の処理
  if (categoryVal !== "") {
   let selectedTemplate = $(`#medium_category_${categoryVal}`); //呼び出すtamplateのidセット
  
   $('#medium_category').remove(); //デフォルト表示用の中カテゴリを削除
   $('#small_category').remove(); //デフォルト表示用の小カテゴリを削除
   $("#selected_medium_category").remove(); //前に選択した中カテゴリがある場合に削除
   $("#selected_small_category").remove();  //前に選択した小カテゴリがある場合に削除（これがないと、中カテゴリ、小カテゴリが選択された状態で、大カテゴリが変更された場合に小が残ってしまう。）
   $('#medium_category_insert_point').after(selectedTemplate.html());　//大カテゴリに紐づいた中カテゴリセレクトを追加
   $('#small_category_insert_point').after(defaultSmallCategorySelect); //デフォルト表示の小カテゴリを追加

  }else {

   //親要素のセレクトボックスが変更されてvalueに値が入っていない場合（include_blankの部分を選択している場合）
   $("#selected_medium_category").remove();//前に選択した中カテゴリがある場合に削除
   $("#selected_small_category").remove(); //前に選択した小カテゴリがある場合に削除
   $('#medium_category').remove();//デフォルト表示用の中カテゴリを削除
   $('#small_category').remove(); //デフォルト表示用の小カテゴリを削除
   $('#medium_category_insert_point').after(defaultMediumCategorySelect); //デフォルト表示の中カテゴリを追加
   $('#small_category_insert_point').after(defaultSmallCategorySelect); //デフォルト表示の小カテゴリを追加
 
  };
 });

//小カテゴリの処理
 $(document).on('change', '#post_medium_category_id', function() {
  let categoryVal = $('#post_medium_category_id').val();
  //親要素のセレクトボックスが変更されてvalueに値が入った場合の処理
  if (categoryVal !== "") {
   let selectedTemplate = $(`#small_category_${categoryVal}`);
   //デフォルトで入っていた子要素のセレクトボックスを削除
  $("#selected_small_category").remove();//前に選択した小カテゴリがある場合に削除
   $('#small_category').remove(); //デフォルト表示の小カテゴリを追加
   // $('#before_medium_category_select_box').remove();
   $('#small_category_insert_point').after(selectedTemplate.html()); //中カテゴリに紐づいた小カテゴリセレクトを追加
  }else {
  $('#small_category').remove();
   $("#selected_small_category").remove(); //前に選択した小カテゴリを削除
   $('#small_category_insert_point').after(defaultSmallCategorySelect); //デフォルト表示の小カテゴリを追加
  };
 });

}); //$(document).on('turbolinks:load', function()

```

なんでこんなに削除したり、追加していたりしているかというと、カテゴリ変更時にどんどんと新しいテンプレートが追加されて、何個も表示されてしまいます。

反対に未選択になると、消えてしまうなんてこともありました。

試行錯誤しましたので、もっと効率のよい方法があるかもしれません。


### まとめ
今回はAjaxを使わずに、RailsでDBと連動した動的な多段セレクトボックス作りに挑戦してみました。
何か良い方法や間違いがあったらアドバイスいただけると幸いです。

最近はFlutterでアプリなんかも作っているので、よかったら遊びにきてください。
[https://twitter.com/oo_forward](https://twitter.com/oo_forward)