##何をしたか
ショッピングサイトの検索・購入ページなどでよく見かける
「多階層型カテゴリの入力フォームが順に表示される機能」を
ancestryとjQueryを使って実装してみました。
振り返りを兼ねて記事を書いていきます。
##下準備
長いので見たい人だけ展開してください
※コードは載せますがここでは特に説明しません。
※scssは必要ないのですが味気ないので入れました。
$ rails _5.2.4_ new ancestry_sample --database=mysql --skip-test --skip-turbolinks --skip-bundle
$ gem install ancestry jquery-rails haml-rails
$ bundle install
$ rails g model category
$ rails g model item
$ rails g controller categories
$ rails g controller items
rails-ujsより上段にjqueryを追加
//= require jquery
//= require rails-ujs
class CreateCategories < ActiveRecord::Migration[5.2]
def change
create_table :categories do |t|
t.string :name, null: false
t.timestamps
end
add_index :categories, :name
end
end
class CreateItems < ActiveRecord::Migration[5.2]
def change
create_table :items do |t|
t.references :category, null: false, foreign_key: true
t.timestamps
end
end
end
class Category < ApplicationRecord
has_many :items
has_ancestry
end
class Item < ApplicationRecord
belongs_to :category
end
class ItemsController < ApplicationController
def index
@items = Item.all
end
def new
@item = Item.new
@categories = []
@categories.push(Category.new(id: 0,name:"---"))
@categories.concat(Category.where(ancestry: nil))
end
def create
Item.create(item_params)
redirect_to items_path
end
private
def item_params
params.require(:item).permit(:category_id)
end
end
Rails.application.routes.draw do
root "items#new"
resources :items ,only: [:index,:new,:create]
end
.items
=form_with(model:@item,local:true) do |f|
.items__parent
= select_tag 'parent', options_for_select(@categories.pluck(:name,:id))
.items__child
.items__grandchild
= f.submit "登録する",class:"button"
%table
%tr
%td No.
%td 親
%td 子
%td 孫
-@items.each_with_index do |item,i|
%tr
%td
= i+1
%td
=item.category.parent.parent.name
%td
=item.category.parent.name
%td
=item.category.name
%button
=link_to '戻る',new_item_path,class:"button"
ary_tops = [{name: "Tシャツ/カットソー(半袖/袖なし)"},{name: "Tシャツ/カットソー(七分/長袖)"},{name: "その他"}]
ary_jacket = [{name: "テーラードジャケット"},{name: "ノーカラージャケット"},{name: "Gジャン/デニムジャケット"},{name: "その他"}]
ary_shoes = [{name: "スニーカー"},{name: "サンダル"},{name: "その他"}]
lady = Category.create(name: "レディース")
lady_tops = lady.children.create(name: "トップス")
lady_tops.children.create(ary_tops)
lady_jacket = lady.children.create(name: "ジャケット/アウター")
lady_jacket.children.create(ary_jacket)
lady_shoes = lady.children.create(name: "靴")
lady_shoes.children.create(ary_shoes)
men = Category.create(name: "メンズ")
men_tops = men.children.create(name: "トップス")
men_tops.children.create(ary_tops)
men_jacket = men.children.create(name: "ジャケット/アウター")
men_jacket.children.create(ary_jacket)
men_shoes = men.children.create(name: "靴")
men_shoes.children.create(ary_shoes)
$ rails db:create
$ rails db:migrate
$ rails db:seed
*{
font-family: Arial,游ゴシック体,YuGothic,メイリオ,Meiryo,sans-serif;
box-sizing: border-box;
}
%__select-form{
width: 300px;
height: 48px;
background-color: #fff;
border-radius: 4px;
font-size: 16px;
border: 1px solid #ccc;
color: #222;
}
#parent{
@extend %__select-form;
}
#child{
@extend %__select-form;
}
#item_category_id{
@extend %__select-form;
}
.button{
width: 300px;
height: 48px;
background-color: #f5f5f5;
border-radius: 5px;
font-size: 17px;
transition: 0.2s;
text-decoration:none;
line-height: 48px;
color: #222;
}
下準備ここまで。
##いざ、実装
では早速やっていきましょう。
※メインはancestryの値の抽出 → ajax通信のため、js内のhtml作成部分には特に触れません。
####親入力欄変更 → 子入力欄表示
- イベント開始点作成(親カテゴリ"parent"を変更した時にイベント開始)
$(function() {
$("#parent").on("change",function(){
}
}
- ajax通信に必要な値の抽出(selectタグから選択された項目のvalue値を抽出)
$(function() {
$("#parent").on("change",function(){
var int = document.getElementById("parent").value
};
}
- コントローラーへのajax通信処理の記述
$(function() {
$("#parent").on("change",function(){
var int = document.getElementById("parent").value
$.ajax({
url: "/categories",
type: 'GET',
dataType: 'json',
data: {id: int}
})
.done(function() {
})
.fail(function() {
});
});
})
- コントローラー内の処理の記述(ancestryの値が選択した親カテゴリのidと同値のレコードを取得)
def index
@categories = Category.where(ancestry: params[:id])
respond_to do |format|
format.json
end
end
- routeの記述
Rails.application.routes.draw do
root "items#new"
resources :items ,only: [:index,:new,:create]
resources :categories ,only: :index
end
- json.jbulderの作成・記述
json.array! @categories do |category|
json.id category.id
json.name category.name
end
- 返り値と表示処理
$(function() {
function buildHTML(result){
var html =
`<option value= ${result.id}>${result.name}</option>`
return html
}
#省略#
.done(function(categories) {
var insertHTML = `<select name="child" id="child">
<option value=0>---</option>`;
$.each(categories, function(i, category) {
insertHTML += buildHTML(category)
});
insertHTML += `</select>`
$('.items__child').append(insertHTML);
})
.fail(function() {
});
});
})
####子入力欄変更 → 孫入力欄表示
- イベント開始点作成(子カテゴリ"child"を変更した時にイベント開始)
$(function() {
$("#parent").on("change",function(){
var int = document.getElementById("parent").value
#中略#
});
$("#child").on("change",function(){
});
})
- コントローラーでバインドするancestryの値「'親id'/'子id'」を取得、およびコントローラーへのajax通信処理を記述
#省略#
$("#child").on("change",function(){
var intParent = document.getElementById("parent").value
var intChild = document.getElementById("child").value
var int = intParent + '/' + intChild
$.ajax({
url: "/categories",
type: 'GET',
dataType: 'json',
data: {id: int}
})
.done(function() {
})
.fail(function() {
});
});
})
※ controller.rb、route.rb、json.jbuilderは前述のものを使用するため割愛します
- 返り値と表示処理
#省略#
.done(function(categories) {
var insertHTML = `<select name="item[category_id]" id="item_category_id">
<option value=0>---</option>`;
$.each(categories, function(i, category) {
insertHTML += buildHTML(category)
});
insertHTML += `</select>`
$('.items__grangchild').append(insertHTML);
.fail(function() {
});
});
})
###完成?
完成!
と言いたいところですが、このままだと親や子を変更する度に入力欄が無限に増殖してしまいます。
- 条件式を追加
- 「"---"を選択した時」 → 下位の要素をremove
- 「追加する要素が既に存在する時」 → 要素をreplace
- それ以外 → append
##省略##
$("#parent").on("change",function(){
var int = document.getElementById("parent").value
if(int == 0){
$('#child').remove();
$('#item_category_id').remove();
}else{
$.ajax({
url: "/categories",
type: 'GET',
dataType: 'json',
data: {id: int}
})
.done(function(categories) {
var insertHTML = `<select name="child" id="child">
<option value=0>---</option>`;
$.each(categories, function(i, category) {
insertHTML += buildHTML(category)
});
insertHTML += `</select>`
if($('#child').length){
$('#child').replaceWith(insertHTML);
$('#item_category_id').remove();
} else {
$('.items__child').append(insertHTML);
};
})
##中略##
$(document).on("change","#child",function(){
var intParent = document.getElementById("parent").value
var intChild = document.getElementById("child").value
var int = intParent + '/' + intChild
if(intChild == 0){
$('#item_category_id').remove();
} else {
$.ajax({
url: "/categories",
type: 'GET',
dataType: 'json',
data: {id: int}
})
.done(function(categories) {
var insertHTML = `<select name="item[category_id]" id="item_category_id">
<option value=0>---</option>`;
$.each(categories, function(i, category) {
insertHTML += buildHTML(category)
});
insertHTML += `</select>`
if($('#item_category_id').length){
$('#item_category_id').replaceWith(insertHTML);
} else {
$('.items__grandchild').append(insertHTML);
};
})
##後略##
というわけで、
完成です!
##注意事項
**・**エラー処理は何もしていないので、保存ができない場合が多々ありますが仕様です。
(孫まで入力しないと、form_withで送信するパラメータを拾えないのでレコード登録できません。)
以上です。
##参考にさせていただいた記事
多階層カテゴリでancestryを使ったら便利すぎた