この記事は以下の記事の続きとなっております。
『ancestryによる多階層構造データを用いて、動的カテゴリーセレクトボックスを実現する~Ajax~』
atora1992.hatenablog.com
やりたいこと
概略
準備
- products_sizesテーブルを作成する
- ancestryを用いて、サイズの種類(親に当たる)ごとにデータを作成する
- カテゴリーの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の導入方法と、データの作成方法は以下の記事を参考にしてください。
カテゴリーは以下の記事で作成した想定です。
products_sizesテーブルでは、親(ancestry = nil)にサイズの種類、それと紐付くようにサイズの詳細をデータで入れておきます。
category_sizesテーブルでは、カテゴリーレコードのidとサイズの親のidを紐づけています。カテゴリーidは親であろうと、子であろうと、孫であろうと問題ありません。サイズと紐づけたいものを入力してください。
実装
- 孫カテゴリーが選択される
- その変化でイベントが発火する(category.js)
- 選択された情報を取得しAjax通信を開始する(category.js)
- 選択されたカテゴリーに紐付くサイズのインスタンスがあれば、中間テーブルを用いてその配列を取得し、json形式に加工する(products_controller.rb)
- サイズ配列を元に、セレクトボックスを作成する(category.js)
コード
resources :products, only: [:index, :show, :new, :create, :edit, :update, :destroy] do collection do ~省略~ get 'get_size', defaults: { format: 'json' } end end
class CategorySize < ApplicationRecord belongs_to :category belongs_to :products_size end
class Category < ApplicationRecord has_many :products has_many :category_sizes has_many :products_sizes, through: :category_sizes has_ancestry end
class ProductsSize < ApplicationRecord has_many :products has_many :category_sizes has_many :categories, through: :category_sizes has_ancestry end
$(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(); } }); });
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
json.array! @sizes do |size| json.id size.id json.size size.size end
細かく見ていこう
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
class CategorySize < ApplicationRecord belongs_to :category belongs_to :products_size end
class Category < ApplicationRecord has_many :products has_many :category_sizes has_many :products_sizes, through: :category_sizes has_ancestry end
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が空の場合は、特に何もしない仕様にしています。
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>`
を参照してください。
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
まずは、選択された孫カテゴリーのレコードを取得します。
selected_grandchild = Category.find("#{params[:grandchild_id]}")
ここから、第一段階の場合わけとして、孫カテゴリーと紐付くサイズがあるかどうかを判断します。
ある場合は、中間テーブルを介してサイズの親のレコードを取得し、ancestryのメソッド.childrenでサイズの詳細の配列を取得します。
if related_size_parent = selected_grandchild.products_sizes[0] #孫カテゴリーと紐付くサイズ(親)があれば取得 @sizes = related_size_parent.children #紐づいたサイズ(親)の子供の配列を取得 else
孫カテゴリーと紐付くサイズがない場合には、第二段階の場合わけに入ります。
今度は、選択された子カテゴリーと紐付くサイズがあるかどうかを判断します。
子カテゴリーのレコードは、孫カテゴリーのレコードに対してancestryのメソッド.parentで取得します。
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を導入しておけば、スムーズに関連するデータを取り出すことができます。
今回は、孫カテゴリーと子カテゴリーのみをサイズと紐づけましたが、親カテゴリーに対して行っても、コントローラの記述(場合わけ)を増やすだけで対応できるかと思います。