選択されたカテゴリーに応じて、動的に変化するサイズセレクトボックスを作成してみた

この記事は以下の記事の続きとなっております。
『ancestryによる多階層構造データを用いて、動的カテゴリーセレクトボックスを実現する~Ajax~』
https://qiita.com/ATORA1992/items/bd824f5097caeee09678

#やりたいこと
カテゴリーに応じたサイズ選択欄を生成したい。
[![Image from Gyazo](https://i.gyazo.com/305fedc4c4cb00fa8f0c5ba50eb24c6a.gif)](https://gyazo.com/305fedc4c4cb00fa8f0c5ba50eb24c6a)
[![Image from Gyazo](https://i.gyazo.com/7d0e03dfe485f225dc5e8f4f876976c3.gif)](https://gyazo.com/7d0e03dfe485f225dc5e8f4f876976c3)

#概略

##準備
1. products_sizesテーブルを作成する
2. ancestryを用いて、サイズの種類（親に当たる）ごとにデータを作成する
3. カテゴリーのidとサイズ（親）のidを結びつける中間テーブル（category_sizesテーブル）を作成する

作成するテーブルは以下の通りです。
**products_sizesテーブル**

| id | size | ancestry |
|--:|:--|:--|
| 1 | 洋服のサイズ | nil |
| 2 | XXS以下 | 1 |
| 3 | XS(SS) | 1 |
| 4 | S | 1 |
|:|:|:|
| 12 | メンズ靴のサイズ | nil |
| 13 | 23.5 cm以下 | 12 |
| 14 | 24 cm | 12 |
| 15 | 24.5cm | 12 |
| 16 | 25 cm | 12 |
|:|:|:|

**category_sizesテーブル**

| id | category_id | products_size_id |
|--:|:--|:--|
| 1 | 2 | 1 |
| 2 | 22 | 1 |
| 3 | 270 | 12 |
|:|:|:|

ancestryの導入方法と、データの作成方法は以下の記事を参考にしてください。
カテゴリーは以下の記事で作成した想定です。
https://qiita.com/ATORA1992/items/03eb78e212080072ab9f
https://qiita.com/ATORA1992/items/617088f885117532454e

*products_sizes*テーブルでは、親（ancestry = nil)にサイズの種類、それと紐付くようにサイズの詳細をデータで入れておきます。
*category_sizes*テーブルでは、カテゴリーレコードのidとサイズの親のidを紐づけています。カテゴリーidは親であろうと、子であろうと、孫であろうと問題ありません。サイズと紐づけたいものを入力してください。

##実装
1. 孫カテゴリーが選択される
2. その変化でイベントが発火する（category.js)
3. 選択された情報を取得しAjax通信を開始する（category.js)
4. 選択されたカテゴリーに紐付くサイズのインスタンスがあれば、中間テーブルを用いてその配列を取得し、json形式に加工する（products_controller.rb)
5. サイズ配列を元に、セレクトボックスを作成する（category.js)



#コード
```routes.rb
 resources :products, only: [:index, :show, :new, :create, :edit, :update, :destroy] do
    collection do
      ~省略~
      get 'get_size', defaults: { format: 'json' }
    end
  end
```
```category_size.rb
class CategorySize < ApplicationRecord
  belongs_to :category
  belongs_to :products_size
end
```
```category.rb
class Category < ApplicationRecord
   has_many :products
   has_many :category_sizes
   has_many :products_sizes, through: :category_sizes
   has_ancestry
end
```
```products_size.rb
class ProductsSize < ApplicationRecord
  has_many :products
  has_many :category_sizes
  has_many :categories, through: :category_sizes
  has_ancestry
end
```
```category.js
$(function(){
  // サイズセレクトボックスのオプションを作成
  function appendSizeOption(size){
    var html = `<option value="${size.size}">${size.size}</option>`;
    return html;
  }
  // サイズ・ブランド入力欄の表示作成
  function appendSizeBox(insertHTML){
    var sizeSelectHtml = '';
    sizeSelectHtml = `<div class="listing-product-detail__size" id= 'size_wrapper'>
                        <label class="listing-default__label" for="サイズ">サイズ</label>
                        <span class='listing-default--require'>必須</span>
                        <div class='listing-select-wrapper__added--size'>
                          <div class='listing-select-wrapper__box'>
                            <select class="listing-select-wrapper__box--select" id="size" name="size_id>
                              <option value="---">---</option>
                              ${insertHTML}
                            <select>
                            <i class='fas fa-chevron-down listing-select-wrapper__box--arrow-down'></i>
                          </div>
                        </div>
                      </div>`;
    $('.listing-product-detail__category').append(sizeSelectHtml);
  }
  // 孫カテゴリー選択後のイベント
  $('.listing-product-detail__category').on('change', '#grandchild_category', function(){
    var grandchildId = $('#grandchild_category option:selected').data('category'); //選択された孫カテゴリーのidを取得
    if (grandchildId != "---"){ //孫カテゴリーが初期値でないことを確認
      $.ajax({
        url: 'get_size',
        type: 'GET',
        data: { grandchild_id: grandchildId },
        dataType: 'json'
      })
      .done(function(sizes){
        $('#size_wrapper').remove(); //孫が変更された時、サイズ欄以下を削除する
        $('#brand_wrapper').remove();
        if (sizes.length != 0) {
        var insertHTML = '';
          sizes.forEach(function(size){
            insertHTML += appendSizeOption(size);
          });
          appendSizeBox(insertHTML);
        }
      })
      .fail(function(){
        alert('サイズ取得に失敗しました');
      })
    }else{
      $('#size_wrapper').remove(); //孫カテゴリーが初期値になった時、サイズ欄以下を削除する
      $('#brand_wrapper').remove();
    }
  });
});
```

```products_controller.rb
class ProductsController < ApplicationController
   # 孫カテゴリーが選択された後に動くアクション
   def get_size
      selected_grandchild = Category.find("#{params[:grandchild_id]}") #孫カテゴリーを取得
      if related_size_parent = selected_grandchild.products_sizes[0] #孫カテゴリーと紐付くサイズ（親）があれば取得
         @sizes = related_size_parent.children #紐づいたサイズ（親）の子供の配列を取得
      else
         selected_child = Category.find("#{params[:grandchild_id]}").parent #孫カテゴリーの親を取得
         if related_size_parent = selected_child.products_sizes[0] #孫カテゴリーの親と紐付くサイズ（親）があれば取得
            @sizes = related_size_parent.children #紐づいたサイズ（親）の子供の配列を取得
         end
      end
   end
end
```
```get_size.json.jbuilder
json.array! @sizes do |size|
  json.id size.id
  json.size size.size
end
```
##細かく見ていこう
###routes.rb
```routes.rb
 resources :products, only: [:index, :show, :new, :create, :edit, :update, :destroy] do
    collection do
      ~省略~
      get 'get_size', defaults: { format: 'json' }
    end
  end
```
Ajaxで動かすアクション用のルートを設定する。

```.rb
defaults: { format: 'json' }
```
で、アクションのリスポンスをjsonに限定しています。
###model
```category_size.rb
class CategorySize < ApplicationRecord
  belongs_to :category
  belongs_to :products_size
end
```
```category.rb
class Category < ApplicationRecord
   has_many :products
   has_many :category_sizes
   has_many :products_sizes, through: :category_sizes
   has_ancestry
end
```
```products_size.rb
class ProductsSize < ApplicationRecord
  has_many :products
  has_many :category_sizes
  has_many :categories, through: :category_sizes
  has_ancestry
end
```
各モデルでアソシエーションを記述します。
中間テーブルのアソシエーションの記述で、*through*のみ書いて、*has_many :category_sizes*を書くのを忘れやすいので注意してください。

###category.js
基本的には、選択された孫カテゴリーのidを取得して、コントローラのアクション*get_size*にidを送り、返ってきたjsonを用いてセレクトボックスを作成するだけです。
ただし、サイズ欄自体が不必要なカテゴリーもあったので、下記の部分で場合わけをし、コントローラからのjsonが空の場合は、特に何もしない仕様にしています。

```category.js
if (sizes.length != 0)
```

また、孫カテゴリーセレクトボックスのHTMLは以下のように実装しています。

```.html
<div class='listing-select-wrapper__added' id= 'grandchildren_wrapper'>
   <div class='listing-select-wrapper__box'>
      <select class="listing-select-wrapper__box--select" id="grandchild_category" name="prefecture">
         <option value="---" data-category="---">---</option>
         <option value="カテゴリーの名前" data-category="カテゴリーid">カテゴリーの名前</option>
      </select>
      <i class='fas fa-chevron-down listing-select-wrapper__box--arrow-down'></i>
   </div>
</div>`
```
https://qiita.com/ATORA1992/items/bd824f5097caeee09678
を参照してください。
###products_controller.rb
今回一番重要なのは、コントローラでカテゴリーに紐付くサイズレコードの配列を取得する部分です。
自身の実装では、孫カテゴリーと結びつくサイズもあれば、子カテゴリーと紐付くサイズもあったので、この両者で場合わけしています。

```products_controller.rb
class ProductsController < ApplicationController
   # 孫カテゴリーが選択された後に動くアクション
   def get_size
      selected_grandchild = Category.find("#{params[:grandchild_id]}") #JSから送られてきた、孫カテゴリーのidを元に、選択された孫カテゴリーのレコードを取得
      if related_size_parent = selected_grandchild.products_sizes[0] #孫カテゴリーと紐付くサイズ（親）があれば取得
         @sizes = related_size_parent.children #紐づいたサイズ（親）の子供の配列を取得
      else
         selected_child = Category.find("#{params[:grandchild_id]}").parent #選択された孫カテゴリーの親（子カテゴリー）のレコードを取得
         if related_size_parent = selected_child.products_sizes[0] #子カテゴリーと紐付くサイズ（親）があれば取得
            @sizes = related_size_parent.children #紐づいたサイズ（親）の子供の配列を取得
         end
      end
   end
end
```

まずは、選択された孫カテゴリーのレコードを取得します。

```products_controller.rb
selected_grandchild = Category.find("#{params[:grandchild_id]}")
```
ここから、第一段階の場合わけとして、孫カテゴリーと紐付くサイズがあるかどうかを判断します。
ある場合は、中間テーブルを介してサイズの親のレコードを取得し、*ancestry*のメソッド*.children*でサイズの詳細の配列を取得します。

```products_controller.rb
if related_size_parent = selected_grandchild.products_sizes[0] #孫カテゴリーと紐付くサイズ（親）があれば取得
   @sizes = related_size_parent.children #紐づいたサイズ（親）の子供の配列を取得
else
```
孫カテゴリーと紐付くサイズがない場合には、第二段階の場合わけに入ります。
今度は、選択された子カテゴリーと紐付くサイズがあるかどうかを判断します。
子カテゴリーのレコードは、孫カテゴリーのレコードに対して*ancestry*のメソッド*.parent*で取得します。

```products_controller.rb
else
    selected_child = Category.find("#{params[:grandchild_id]}").parent #選択された孫カテゴリーの親（子カテゴリー）のレコードを取得
    if related_size_parent = selected_child.products_sizes[0] #子カテゴリーと紐付くサイズ（親）があれば取得
       @sizes = related_size_parent.children #紐づいたサイズ（親）の子供の配列を取得
    end
end
```
#まとめ
肝は、**中間テーブルでカテゴリーとサイズを紐づけておく**ことです。
*ancestry*を導入しておけば、スムーズに関連するデータを取り出すことができます。
今回は、孫カテゴリーと子カテゴリーのみをサイズと紐づけましたが、親カテゴリーに対して行っても、コントローラの記述（場合わけ）を増やすだけで対応できるかと思います。