23
32

More than 5 years have passed since last update.

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

Posted at

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

やりたいこと

カテゴリーに応じたサイズ選択欄を生成したい。
Image from Gyazo
Image from Gyazo

概略

準備

  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で動かすアクション用のルートを設定する。

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は以下のように実装しています。

<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を導入しておけば、スムーズに関連するデータを取り出すことができます。
今回は、孫カテゴリーと子カテゴリーのみをサイズと紐づけましたが、親カテゴリーに対して行っても、コントローラの記述(場合わけ)を増やすだけで対応できるかと思います。

23
32
3

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
23
32