#はじめに
某プログラミングスクールの最終課題でフリマアプリのクローンを作成中です。
商品出品機能に必要な「カテゴリ選択機能」の実装において作業した内容について記載していきます。
- JavaScriptを用いたプルダウン選択フォームの作成
※この前段階として、ancestryを使って使用するカテゴリデータのテーブルを作っています。
その記事はコチラ▶︎ancestryを用いた階層型カテゴリデータの作成方法
#この記事でわかること
- Ajax通信で3段階のカテゴリ選択フォームを表示させる
#この機能で達成したいゴール
下記のように親要素(レディース)選択したら、子要素の選択フォームが、子要素を選択したら孫要素の選択フォームが非同期通信で表示されるようにします。
また、子や孫の選択肢を変えたらフォーム内容が初期化されたり、フォームそのものが消えたりします。
#開発環境
- ruby 2.6.5
- rails 6.0.3.2
- sequel pro
#大まかな流れ
0. gemのインストール
- JSを使うためのコントローラー、JSファイルを作成し、呼び出しの記述をする。
- ルーティングを追加する。
- コントローラーにデータを検索する#search機能を設定する。
- jsファイルを編集。
- Ajax通信時に用いるjson.jbuilderファイルを編集。
- 上記4−6を子要素、孫要素で繰り返す。
- 子要素・孫要素を変更した際のアクションを追加する。
- 完成
前提として、
- ancestryを用いたデータ作成は完了していることとします。
*ancestryを使ったカテゴリデータ作成方法はコチラをクリック - itemsコントローラーも作成済とする。
#具体的な実装手順
0. gemのインストール
まずは必要なgemをインストール。
gem jquery-rails
bundle install
1.JavaScriptを使うための記述をする
require('jquery')
require('item/category')
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
jquery: 'jquery',
})
)
module.exports = environment
ここで早速つまづいたのが、わたしの開発環境がrails6だったこと。
ネットで調べた記事のほとんどがrails5以前での情報になっており、
rails6ではapplication.jsファイルの生成場所や呼び出しのための記述が違うらしく、
最初うまく読み込まれなくて混乱しました。
開発の際はバージョンを確認の上、各自の環境にあわせて記述してください。
rails6でのjquery導入はこの記事を参考にさせていただきました。=>rails6でのjqueryの導入方法
2.ルーティングを追加
のちほどitemsコントローラーにsearchアクションを追加するため、
searchアクションへのルーティングを追記します。
Rails.application.routes.draw do
resources :items do
collection do
get :search
end
end
end
3.ビューにカテゴリ選択フォームを作成
こちらは、あくまで私の記述なの参考までに。
このような形でまずは親カテゴリ用のフォームを作成しました。
=form_with model @item do |f|
= f.collection_select :category_id, @parents, :id, :name, {prompt: "選択してください"}, {class: "categoryForm", id: "category_form"}
4.コントローラーにsearchアクションを追加
newアクションでレコードを取得。
def new
@item = Item.new
@item_image = ItemImage.new
end
また、newアクションとcreateアクションの前に親要素の情報を取得しておきたいので、before_actionを設定しました。
before_action :set_parents, only: [:new, :create]
def set_parents
@parents = Category.where(ancestry: nil)
end
ancestry=nil、つまり親要素の値を取得し、変数@parentsへ代入しています。
newだけではなく、createの時にもset_parentsが必要な理由は、
コチラの記事を参考にさせていただきました。
(正直、コード書いている時はわかったような、わからないような...でした。)
そして、子・孫のカテゴリ検索のため、searchアクションを設定します。
def search
respond_to do |format|
format.html
#ajax通信開始
format.json do
#子カテゴリの情報を@childrensに代入
@childrens = Category.find(params[:parent_id]).children
end
end
end
余談ですが、複数形の"children"に更に"s"を付けるのにどうしても違和感があったのですが、
childとchildrenを使いわけるのもわかりにくいなぁと思い、仕方なくchildrensにしました。
5.json.jbulderファイルを作成
json.array! @childrens do |children|
json.id children.id
json.name children.name
end
とりあえず、いまは子要素の分のみ記載しています。
6.jsファイルを編集し、上記アクションで送られてくるデータを受け取る
1.まずは、親要素を選択した後に現れる「子要素の選択フォーム」を作成。
// 子要素を選択するフォーム
function add_childSelect_tag() {
let child_select_form = `
<select name="item[category_id]" id="item_category_id" class="child_category_id">
<option value="">カテゴリを選択</option>
</select>
`
return child_select_form;
}
このHTMLの部分を書くのが苦手なのですが、ビューの検証ツールで親要素のフォームをみて書くのが一番簡単な気がしています。
name="item[category_id]はデータの送り先です。
あとで、子要素のフォームを消したり初期化したりしたい(親要素を変更したときなど)ので、子要素特有のclass名も追加しました。
2.表示された選択フォームにデータを取得するためのoptionを記載。
function add_Option(children) {
let option_html = `
<option value=${children.id}>${children.name}</option>
`
return option_html;
}
3.親カテゴリを選択した後に起こるイベントを設定する。
//親カテゴリを選択したあとのイベント
$("#category_form").on("change", function() {
let parentValue = $("#category_form").val();
if (parentValue.length !== 0) {
$.ajax({
url: '/items/search',
type: 'GET',
data: { parent_id: parentValue},
dataType: 'json'
})
.done(function (data){
let child_select_form = add_childSelect_tag
$(".ItemInfo__category--form").append(child_select_form);
data.forEach(function(d){
let option_html = add_Option(d)
$(".child_category_id").append(option_html);
});
})
.fail(function (){
alert("カテゴリ取得に失敗しました");
});
おおまかな流れとしては、
- 親要素のデータを取得し、parentValueへ代入。
- if〜でデータが初期値でなければajax通信。
- .doneでappendを使って、親要素のフォームの後に前手順で作成した子要素のフォームを表示させる。
- optionでとりだしたデータをひとつずつ取り出して表示させる。
###これで子要素の表示は完成!
親要素を選択すれば、子要素フォームが表示されます。
4.孫要素の表示を作成する。
基本的には、子要素の手順を繰り返すだけです。
json.array! @grandchildrens do |grandchildren|
json.id grandchildren.id
json.name grandchildren.name
end
// 孫要素の選択フォーム
function add_grandchildSelect_tag(){
let grandchild_select_form = `
<select name="item[category_id]" id="item_category_id" class="grandchild_category_id">
<option value="">カテゴリを選択</option>
</select>
`
return grandchild_select_form
}
// 子カテゴリを選択後のイベント
$(document).on("change", ".child_category_id", function(){
let childValue = $(".child_category_id").val();
if (childValue.length !=0){
$.ajax({
url: '/items/search',
type: 'GET',
data: { children_id: childValue},
dataType: 'json'
})
.done(function (gc_data){
let grandchild_select_form = add_grandchildSelect_tag
$(".ItemInfo__category--form").append(grandchild_select_form);
gc_data.forEach(function (gc_d){
let option_html = add_Option(gc_d);
$(".grandchild_category_id").append(option_html);
})
})
.fail(function (){
alert("カテゴリ取得に失敗しました");
});
})
これで、子要素を選択すれば孫要素の選択フォームがでてくるようになります。
##5.フォームを初期化する記述を追加する。
1-4の手順で一見完成したように見えるのですが、
このままだと以下のような不具合がでます。
- 親や子カテゴリを変更しても子カテゴリの内容がそのまま。
- 子や孫カテゴリを変更すると、孫の下に更に新たなカテゴリが出現してしまう。
要するに、子や孫カテゴリをいじっていると、エンドレスでフォームが追加されていってしまう状態。
なので、望ましい挙動として、
- 親カテゴリを変更したら、孫カテゴリフォームは消えて、子カテゴリのフォームの内容は親カテゴリに紐づいた内容に変わる。
- 子カテゴリを変更したら、孫カテゴリの内容は子カテゴリに紐づいたデータに変わる。
- 親や子カテゴリを初期値(選んでいない状態)にしたら、その次の階層のフォームは消える。
この動作を実現させないといけません。
そのために、下記の記述を加えました。
// 親カテゴリを選択したあとのイベント
$("#category_form").on("change", function() {
let parentValue = $("#category_form").val();
if (parentValue.length !== 0) {
$.ajax({
url: '/items/search',
type: 'GET',
data: { parent_id: parentValue},
dataType: 'json'
})
.done(function (data){
$(".child_category_id").remove();
$(".grandchild_category_id").remove();
let child_select_form = add_childSelect_tag
$(".ItemInfo__category--form").append(child_select_form);
data.forEach(function(d){
let option_html = add_Option(d)
$(".child_category_id").append(option_html);
});
})
.fail(function (){
alert("カテゴリ取得に失敗しました");
})
}else{
$(".child_category_id").remove();
$(".grandchild_category_id").remove();
}
});
追加したのは、①.doneのあとのこの2行と
$(".child_category_id").remove();
$(".grandchild_category_id").remove();
②.failのあとにelseで条件分けしたこの部分です。
else{
$(".child_category_id").remove();
$(".grandchild_category_id").remove();
}
①で親カテゴリの内容が変わるたびに子/孫カテゴリを一度消し、その上で子カテゴリを出現させるようにしました。
②のelse文の後ろは、親が初期値を選択した場合。
同じコードを子カテゴリ選択後のイベント部分にも追記しました。
そして、全部ページを読み込んでからこれらのJSを作動させるため、
window.addEventListener('load', function () {
}
これでコード全体を括りました。
###これで本当に完成!!
このあと、うっかりチームメンバーに「db:seedしてね」と言うのを忘れ、
「カテゴリ選択できない!!なぜ?!」という事態もおきましたが、無事に解決にいたりました。
##参考にした記事
[Rails]某フリマアプリのカテゴリー機能の実装方法
フリマアプリのカテゴリ機能〜gem : ancestryを使用〜
[Rails] Ajax通信を用いたカテゴリボックス作成