この記事は以下の記事の続きとなっております。
『ancestryによる多階層構造データを用いて、動的カテゴリーセレクトボックスを実現する~Ajax~』
https://qiita.com/ATORA1992/items/bd824f5097caeee09678
#やりたいこと
カテゴリーに応じたサイズ選択欄を生成したい。
#概略
##準備
- 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の導入方法と、データの作成方法は以下の記事を参考にしてください。
カテゴリーは以下の記事で作成した想定です。
https://qiita.com/ATORA1992/items/03eb78e212080072ab9f
https://qiita.com/ATORA1992/items/617088f885117532454e
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>`
https://qiita.com/ATORA1992/items/bd824f5097caeee09678
を参照してください。
###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を導入しておけば、スムーズに関連するデータを取り出すことができます。
今回は、孫カテゴリーと子カテゴリーのみをサイズと紐づけましたが、親カテゴリーに対して行っても、コントローラの記述(場合わけ)を増やすだけで対応できるかと思います。