近況一言メモ
夏本番に向かって、フリーランスは体が資本ということもあり、
体力をつける・体を作っていかないといけない。
しみじみ、実感するここ最近。
概要
ancestryで作成したカテゴリーデータを用いて、選択肢を動的に変化させる機能を実装しました。
データの作成方法は以下のリンクでご確認ください。
『ancestryによる多階層構造の実現』 atora1992.hatenablog.com 『多階層構造のカテゴリーを実現する、seedを作ってみた』 atora1992.hatenablog.com
作成したテーブルの例
id | name | ancestry |
---|---|---|
1 | メンズ | nil |
2 | トップス | 1 |
3 | すべて | 1/2 |
4 | Tシャツ/カットソー(半袖/袖なし) | 1/2 |
5 | Tシャツ/カットソー(七分/長袖) | 1/2 |
開発環境
Ruby 2.5.1
Rails 5.2.3
jQuery 1.12.4
実装した機能
実際に作った画面です
サイズも出てきていますが、それはまた別の機会に
概略
- 親カテゴリーが選択される
- その変化でイベントが発火する(category.js)
- 選択された情報を取得しAjax通信を開始する(category.js)
- 選択されたカテゴリーの子カテゴリーの配列をjsonで取得する(products_controller.rb)
- 子カテゴリー配列を元に、セレクトボックスを作成する(category.js)
- 子カテゴリーが選択される
- その変化でイベントが発火する(category.js)
- 選択された情報を取得しAjax通信を開始する(category.js)
- 選択された子カテゴリーの子(孫)カテゴリーの配列をjsonで取得する(products_controller.rb)
- 孫カテゴリー配列を元に、セレクトボックスを作成する(category.js)
コード
resources :products, only: [:index, :show, :new, :edit, :destroy] do #Ajaxで動くアクションのルートを作成 collection do get 'get_category_children', defaults: { format: 'json' } get 'get<div class="code-title" data-title="Gemfile">_category_grandchildren', defaults: { format: 'json' } end end
class ProductsController < ApplicationController def new #セレクトボックスの初期値設定 @category_parent_array = ["---"] #データベースから、親カテゴリーのみ抽出し、配列化 Category.where(ancestry: nil).each do |parent| @category_parent_array << parent.name end end # 以下全て、formatはjsonのみ # 親カテゴリーが選択された後に動くアクション def get_category_children #選択された親カテゴリーに紐付く子カテゴリーの配列を取得 @category_children = Category.find_by(name: "#{params[:parent_name]}", ancestry: nil).children end # 子カテゴリーが選択された後に動くアクション def get_category_grandchildren #選択された子カテゴリーに紐付く孫カテゴリーの配列を取得 @category_grandchildren = Category.find("#{params[:child_id]}").children end end
json.array! @category_children do |child| json.id child.id json.name child.name end
json.array! @category_grandchildren do |grandchild| json.id grandchild.id json.name grandchild.name end
.listing-form-box .listing-product-detail__category = f.label 'カテゴリー', class: 'listing-default__label' %span.listing-default--require 必須 .listing-select-wrapper .listing-select-wrapper__box = f.select :category, @category_parent_array, {}, {class: 'listing-select-wrapper__box--select', id: 'parent_category'} %i.fas.fa-chevron-down.listing-select-wrapper__box--arrow-down
$(function(){ // カテゴリーセレクトボックスのオプションを作成 function appendOption(category){ var html = `<option value="${category.name}" data-category="${category.id}">${category.name}</option>`; return html; } // 子カテゴリーの表示作成 function appendChidrenBox(insertHTML){ var childSelectHtml = ''; childSelectHtml = `<div class='listing-select-wrapper__added' id= 'children_wrapper'> <div class='listing-select-wrapper__box'> <select class="listing-select-wrapper__box--select" id="child_category" name="category_id"> <option value="---" data-category="---">---</option> ${insertHTML} <select> <i class='fas fa-chevron-down listing-select-wrapper__box--arrow-down'></i> </div> </div>`; $('.listing-product-detail__category').append(childSelectHtml); } // 孫カテゴリーの表示作成 function appendGrandchidrenBox(insertHTML){ var grandchildSelectHtml = ''; grandchildSelectHtml = `<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="category_id"> <option value="---" data-category="---">---</option> ${insertHTML} <select> <i class='fas fa-chevron-down listing-select-wrapper__box--arrow-down'></i> </div> </div>`; $('.listing-product-detail__category').append(grandchildSelectHtml); } // 親カテゴリー選択後のイベント $('#parent_category').on('change', function(){ var parentCategory = document.getElementById('parent_category').value; //選択された親カテゴリーの名前を取得 if (parentCategory != "---"){ //親カテゴリーが初期値でないことを確認 $.ajax({ url: 'get_category_children', type: 'GET', data: { parent_name: parentCategory }, dataType: 'json' }) .done(function(children){ $('#children_wrapper').remove(); //親が変更された時、子以下を削除するする $('#grandchildren_wrapper').remove(); $('#size_wrapper').remove(); $('#brand_wrapper').remove(); var insertHTML = ''; children.forEach(function(child){ insertHTML += appendOption(child); }); appendChidrenBox(insertHTML); }) .fail(function(){ alert('カテゴリー取得に失敗しました'); }) }else{ $('#children_wrapper').remove(); //親カテゴリーが初期値になった時、子以下を削除するする $('#grandchildren_wrapper').remove(); $('#size_wrapper').remove(); $('#brand_wrapper').remove(); } }); // 子カテゴリー選択後のイベント $('.listing-product-detail__category').on('change', '#child_category', function(){ var childId = $('#child_category option:selected').data('category'); //選択された子カテゴリーのidを取得 if (childId != "---"){ //子カテゴリーが初期値でないことを確認 $.ajax({ url: 'get_category_grandchildren', type: 'GET', data: { child_id: childId }, dataType: 'json' }) .done(function(grandchildren){ if (grandchildren.length != 0) { $('#grandchildren_wrapper').remove(); //子が変更された時、孫以下を削除するする $('#size_wrapper').remove(); $('#brand_wrapper').remove(); var insertHTML = ''; grandchildren.forEach(function(grandchild){ insertHTML += appendOption(grandchild); }); appendGrandchidrenBox(insertHTML); } }) .fail(function(){ alert('カテゴリー取得に失敗しました'); }) }else{ $('#grandchildren_wrapper').remove(); //子カテゴリーが初期値になった時、孫以下を削除する $('#size_wrapper').remove(); $('#brand_wrapper').remove(); } }); });
細かく見ていこう
routes.rb
resources :products, only: [:index, :show, :new, :edit, :destroy] do #Ajaxで動くアクションのルートを作成 collection do get 'get_category_children', defaults: { format: 'json' } get 'get_category_grandchildren', defaults: { format: 'json' } end end
Ajaxで動かすアクション用のルートを設定する。
defaults: { format: 'json' }
で、アクションのリスポンスをjsonに限定しています。
new.html.haml
.listing-form-box .listing-product-detail__category = f.label 'カテゴリー', class: 'listing-default__label' %span.listing-default--require 必須 .listing-select-wrapper .listing-select-wrapper__box //親カテゴリーのセレクトボックスの生成 = f.select :category, @category_parent_array, {}, {class: 'listing-select-wrapper__box--select', id: 'parent_category'} %i.fas.fa-chevron-down.listing-select-wrapper__box--arrow-down
ビューでは、初期値の親カテゴリーボックスのみ記載する。
セレクトボックスの選択肢は、products_controller.rbで作成したインスタンス変数を利用する。
※jbuilderの2ファイルは、new.html.hamlと同じフォルダーに入れてください。
products_controller.rb
def new #セレクトボックスの初期値設定 @category_parent_array = ["---"] #データベースから、親カテゴリーのみ抽出し、配列化 Category.where(ancestry: nil).each do |parent| @category_parent_array << parent.name end end
アクションnewで、親カテゴリーの選択肢配列を作成する。
親カテゴリーのパス(カラム名:ancestry)の値は"nil"なので、".where"メソッドで検索をかける。
検索でヒットしたインスタンスを一つずつ取り出し、名前のみ配列に追加。
# 親カテゴリーが選択された後に動くアクション def get_category_children #選択された親カテゴリーに紐付く子カテゴリーの配列を取得 @category_children = Category.find_by(name: "#{params[:parent_name]}", ancestry: nil).children end # 子カテゴリーが選択された後に動くアクション def get_category_grandchildren #選択された子カテゴリーに紐付く孫カテゴリーの配列を取得 @category_grandchildren = Category.find("#{params[:child_id]}").children end
routes.rbでjsonに限定したので、通常のアクションと同じ書き方でOK
jbuilderに渡す変数を作成する。
ancestryを導入しているので、".children"メソッドで、選択されたものの子カテゴリーの配列を取得する。
category.js
まずは、親カテゴリーが選択された時の挙動から。
$(function(){ // カテゴリーセレクトボックスのオプションを作成 function appendOption(category){ var html = `<option value="${category.name}" data-category="${category.id}">${category.name}</option>`; return html; } // 子カテゴリーの表示作成 function appendChidrenBox(insertHTML){ var childSelectHtml = ''; childSelectHtml = `<div class='listing-select-wrapper__added' id= 'children_wrapper'> <div class='listing-select-wrapper__box'> <select class="listing-select-wrapper__box--select" id="child_category" name="category_id"> <option value="---" data-category="---">---</option> ${insertHTML} <select> <i class='fas fa-chevron-down listing-select-wrapper__box--arrow-down'></i> </div> </div>`; $('.listing-product-detail__category').append(childSelectHtml); } // 親カテゴリー選択後のイベント $('#parent_category').on('change', function(){ var parentCategory = document.getElementById('parent_category').value; //選択された親カテゴリーの名前を取得 if (parentCategory != "---"){ //親カテゴリーが初期値でないことを確認 $.ajax({ url: 'get_category_children', type: 'GET', data: { parent_name: parentCategory }, dataType: 'json' }) .done(function(children){ $('#children_wrapper').remove(); //親が変更された時、子以下を削除するする $('#grandchildren_wrapper').remove(); $('#size_wrapper').remove(); $('#brand_wrapper').remove(); var insertHTML = ''; children.forEach(function(child){ insertHTML += appendOption(child); }); appendChidrenBox(insertHTML); }) .fail(function(){ alert('カテゴリー取得に失敗しました'); }) }else{ $('#children_wrapper').remove(); //親カテゴリーが初期値になった時、子以下を削除するする $('#grandchildren_wrapper').remove(); $('#size_wrapper').remove(); $('#brand_wrapper').remove(); } }); });
以下のコードで、親カテゴリーボックスの変化を検知して、イベントが発火します。
$('#parent_category').on('change', function(){ var parentCategory = document.getElementById('parent_category').value; //選択された親カテゴリーの名前を取得
"document.getElementById('parent_category').value"
で、セレクトボックスで選択されたvalueを取得します。
e.g. トップのgifでいうと、"parentCategory = "レディース""となります。
親カテゴリーで選択されたvalueが初期値でないことを確認して、Ajax通信を開始します。
products_controller.rbで受け取るparamsのキーはparent_nameにしました。
親カテゴリーが初期値になった場合は、子カテゴリー以下のビュー表示を削除します。
if (parentCategory != "---"){ //親カテゴリーが初期値でないことを確認 $.ajax({ url: 'get_category_children', type: 'GET', data: { parent_name: parentCategory }, dataType: 'json' }) ~省略~ }else{ $('#children_wrapper').remove(); //親カテゴリーが初期値になった時、子以下を削除するする $('#grandchildren_wrapper').remove(); $('#size_wrapper').remove(); $('#brand_wrapper').remove(); }
Ajax通信後、jbuilderで成形したjsonを受け取る。(childrenとして命名)
Ajaxに成功した場合にも、子カテゴリー以下の表示をリセットするために、".remove()"を行う。
リセットしたのち、"appendOption"でセレクトボックスの選択肢(optionタグ)をchildrenを元に作成。
作成した選択肢を含めたセレクトボックスを"appendChildrenBox"で作成。
// カテゴリーセレクトボックスのオプションを作成 function appendOption(category){ var html = `<option value="${category.name}" data-category="${category.id}">${category.name}</option>`; return html; } // 子カテゴリーの表示作成 function appendChidrenBox(insertHTML){ var childSelectHtml = ''; childSelectHtml = `<div class='listing-select-wrapper__added' id= 'children_wrapper'> <div class='listing-select-wrapper__box'> <select class="listing-select-wrapper__box--select" id="child_category" name="category_id"> <option value="---" data-category="---">---</option> ${insertHTML} <select> <i class='fas fa-chevron-down listing-select-wrapper__box--arrow-down'></i> </div> </div>`; $('.listing-product-detail__category').append(childSelectHtml); } ~省略~ .done(function(children){ $('#children_wrapper').remove(); //親が変更された時、子以下を削除するする $('#grandchildren_wrapper').remove(); $('#size_wrapper').remove(); $('#brand_wrapper').remove(); var insertHTML = ''; children.forEach(function(child){ insertHTML += appendOption(child); }); appendChidrenBox(insertHTML); }) .fail(function(){ alert('カテゴリー取得に失敗しました'); })
孫カテゴリー作成も同様ですが、重要な変更点が2点あります。
- 変化を捉える方法
- Ajax通信でコントローラーに渡すparamsの中身
(1)子カテゴリーが選択された時にイベントを発火するには、JSで追加したhtml要素を認識する必要があります。
その場合、親カテゴリーと同じように書いても認識してくれません。
親カテゴリーと子カテゴリーのセレクトボックスを含む大きな枠・変化しないもの(今回は"$('.listing-product-detail__category')")に対して".on"でイベントを拾い、発動するべき対象を指定する必要があります。
それが、以下の部分です。
$('.listing-product-detail__category').on('change', '#child_category', function()
(2)Ajaxでコントローラーに渡す値を、カテゴリーの名前そのものではなく、レコードのidにしています。
というのも、名前で検索をかけると、子カテゴリーでは同じ名前が複数あり、意図したものを取得してくれないからです。
レコードのidを取得するために、JSで追加するoptionタグにdata属性を持たせ、data属性を用いて選択されたカテゴリーのレコードidを取得しています。
function appendOption(category){ var html = `<option value="${category.name}" data-category="${category.id}">${category.name}</option>`; return html; } ~省略~ var childId = $('#child_category option:selected').data('category'); //選択された子カテゴリーのidを取得
まとめ
コードが長くなりましたが、概略が理解できれば実装方法は色々あると思います。
自分は、孫カテゴリー作成の注意点(2)を最初せずに、名前で検索をかけていたので、取得されるカテゴリー配列が意図したものにならず、少しつまりました。
特に、セレクトボックスで"選択された"という情報をどう判断したら良いかがわからずつまりました。
(下記コードの"option:selected"の部分)
var childId = $('#child_category option:selected').data('category'); //選択された子カテゴリーのidを取得
長いコードを読んでいただきありがとうございます。
少しでも、誰かの手助けになれば嬉しいです。