目標
実装
1.Gemを導入
# 追記
gem 'ancestry'
$ bundle
2.カラムを追加
データ量が多くなる可能性があるので念の為index
を張っておきます。
$ rails g migration AddAncestryToCategory ancestry:string:index
$ rails db:migrate
create_table "categories", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "ancestry"
t.index ["ancestry"], name: "index_categories_on_ancestry"
end
3.モデルを編集
# 追記
has_ancestry
has_ancestry
➡︎ このクラスでancestryを使える様になる。
メソッド一覧
メソッド名 | 返り値 |
---|---|
parent | 親レコードを取得 |
parent_id | 親レコードのIDを取得 |
root | レコードのルートを取得 |
root_id | レコードのルートIDを取得 |
root?is_root? | レコードがルートであれば、trueを返す |
ancestors | ルートで始まり、親で終わる、レコードの祖先を返す |
ancestors? | レコードに祖先(ルートノードではない)がある場合はtrueを返す |
ancestor_ids | レコードの祖先のIDを返す |
path | ルートで始まり、自己で終わる、レコードのパスを返す |
path_ids | ルートIDで始まり、 自己のIDで終わるパスのIDをリストで返す |
children | 子レコードを取得 |
child_ids | 子レコードのIDを取得 |
has_parent?ancestors? | レコードが親を持っていれば、trueを返す |
has_children?children? | レコードが子を持っていれば、trueを返す |
is_childless?childless? | レコードが子を持っていなければ、trueを返す |
siblings | 兄弟レコード(同じ階層のレコード)を返す |
sibling_ids | 兄弟レコード(同じ階層のレコード)のIDを返す |
has_siblings?siblings? | レコードの親に複数の子がある場合はtrueを返す |
is_only_child?only_child? | レコードが親の唯一の子である場合はtrueを返す |
descendants | 子レコード、孫レコード、曽孫レコード... を返す |
descendant_ids | 子レコード、孫レコード、曽孫レコード... のIDを返す |
indirects | 孫レコード以下を返す |
indirect_ids | 孫レコード以下のIDを返す |
subtree | 子孫と自己のモデルを返す |
subtree_ids | レコードのサブツリーのIDをリストで返す |
depth | ノードの深さを返す |
parent_of?(node) | このレコードを(node)の親にする |
root_of?(node) | このレコードを(node)のルートにする |
ancestor_of?(node) | (node)の祖先にはこのレコードが含まれる |
child_of?(node) | (node)はレコードの親 |
descendant_of?(node) | (node)はこのレコードの祖先の1つ |
indirect_of?(node) | (node)はこのレコードの祖先の1つですが、親ではない |
階層構造
親:ビジネス
子:経済
孫:日本経済、国際経済
本のカテゴリーに上記の様な親子関係を持たせたい場合は、以下の様にデータを作成します。
business = Category.create(name: 'ビジネス')
business_economy = business.children.create(name: '経済')
business_economy.children.create([{ name: '日本経済' }, { name: '国際経済' }])
カラム構造
id | name | ancestry |
---|---|---|
1 | ビジネス | nil |
2 | 経済 | 1(親のid) |
3 | 日本経済 | 1/2(親のid/子のid) |
4 | 国際経済 | 1/2(親のid/子のid) |
2.category.rb
にメソッドを作成
def self.category_parent_array_create
category_parent_array = ['---']
Category.where(ancestry: nil).each do |parent|
category_parent_array << [parent.name, parent.id]
end
return category_parent_array
end
【解説】
① 親カテゴリーのセレクトボックスを作成する為の配列を用意し、初期値を格納する。
category_parent_array = ['---']
② ancestryの値がnil
、つまり親カテゴリーを全て抽出し、①で作成した配列にカテゴリー名とIDを格納する。
Category.where(ancestry: nil).each do |parent|
category_parent_array << [parent.name, parent.id]
end
※重要ポイント
[parent.name, parent.id]
➡︎ ビューで親カテゴリーをセレクトボックスに表示する際、
第1引数(parent.name)がブラウザに表示される値になり、
第2引数(parent.id)がパラメーターとして送信する値になる。
③ ①
で作成した配列を戻り値として返す。
return category_parent_array
3.book_category.rb
にメソッドを作成
def self.maltilevel_category_create(book, parent_id, children_id, grandchildren_id)
if parent_id.present? && parent_id != '---'
category = Category.find(parent_id)
BookCategory.create(book_id: book.id, category_id: category.id)
end
if children_id.present? && children_id != '---'
category = Category.find(children_id)
BookCategory.create(book_id: book.id, category_id: category.id)
end
if grandchildren_id.present? && grandchildren_id != '---'
category = Category.find(grandchildren_id)
BookCategory.create(book_id: book.id, category_id: category.id)
end
end
【解説】
① コントローラーから引数を4つ受け取る。
(book, parent_id, children_id, grandchildren_id)
book
: 作成する本のデータ
parent_id
: 親カテゴリーのID
children_id
: 子カテゴリーのID
grandchildren_id
: 孫カテゴリーのID
② 親カテゴリーのIDを引数として受け取れた場合かつ、初期値でない場合処理を実行する。
if parent_id.present? && parent_id != '---'
③ Categoryモデルから引数で受け取った親カテゴリーIDに対応するレコードを抽出し、変数に代入する。
category = Category.find(parent_id)
④ BookCategory(中間テーブル)にレコードを作成する。**
BookCategory.create(book_id: book.id, category_id: category.id)
4.コントローラーを編集
def create
@book = Book.new(book_params)
@book.user_id = current_user.id
if @book.save
BookCategory.maltilevel_category_create(
@book,
params[:parent_id],
params[:children_id],
params[:grandchildren_id]
)
redirect_to books_path
else
@books = Book.all
@category_parent_array = Category.category_parent_array_create
render 'index'
end
end
def index
@book = Book.new
@books = Book.all
@category_parent_array = Category.category_parent_array_create
end
def get_category_children
@category_children = Category.find(params[:parent_id]).children
end
def get_category_grandchildren
@category_grandchildren = Category.find(params[:children_id]).children
end
【解説】
① maltilevel_category_create
メソッドに引数を4つ渡して実行する。
BookCategory.maltilevel_category_create(
@book,
params[:parent_id],
params[:children_id],
params[:grandchildren_id]
)
◎ Ajax通信で送られてきたパラメーターを受け取る。
params[:parent_id],
params[:children_id],
params[:grandchildren_id]
② category_parent_array_create
メソッドの戻り値として受け取った配列をインスタンス変数に代入する。
@category_parent_array = Category.category_parent_array_create
③ 選択された親カテゴリーに紐付く子カテゴリーを抽出する。
def get_category_children
@category_children = Category.find(params[:parent_id]).children
end
④ 選択された子カテゴリーに紐付く孫カテゴリーを抽出する。
def get_category_grandchildren
@category_grandchildren = Category.find(params[:children_id]).children
end
5.json.jbuilderファイル
を作成・編集
$ touch app/views/books/get_category_children.json.jbuilder
json.array! @category_children do |children|
json.id children.id
json.name children.name
end
$ touch app/views/books/get_category_grandchildren.json.jbuilder
json.array! @category_grandchildren do |grandchildren|
json.id grandchildren.id
json.name grandchildren.name
end
【解説】
① get_category_children
アクションで抽出したレコードを繰り返し処理し、配列を作成する。
json.array! @category_children do |children|
② 各IDと名前を ①
で作成した配列に格納する。
json.id children.id
json.name children.name
◎ 親カテゴリー(ビジネス)が選択された場合の返り値
[
{
"id": 2,
"name": "金融"
},
{
"id": 6,
"name": "経済"
},
{
"id": 9,
"name": "経営"
},
{
"id": 13,
"name": "マーケティング"
},
]
◎ 子カテゴリー(金融)が選択された場合
[
{
"id": 3,
"name": "株"
},
{
"id": 4,
"name": "為替"
},
{
"id": 5,
"name": "税金"
},
]
6.ルーティングを追加
# 追記
get 'get_category/children', to: 'books#get_category_children', defaults: { format: 'json' }
get 'get_category/grandchildren', to: 'books#get_category_grandchildren', defaults: { format: 'json' }
7.ビューを編集
.category-form
= label_tag 'ジャンル'
= select_tag 'parent_id', options_for_select(@category_parent_array), id: 'parent-category', class: 'form-control'
i.fas.fa-chevron-down
br
【解説】
① コントローラーで定義したインスタンス変数内のデータをセレクトボックスに表示し、プロパティ(パラメーター名)をparent_id
に設定する。
= select_tag 'parent_id', options_for_select(@category_parent_array), id: 'parent-category', class: 'form-control'
8.JavaScriptファイルを作成・編集
$ touch app/assets/javascripts/category_form.js
$(function() {
function appendOption(category) {
let html = `<option value='${category.id}' data-category='${category.id}'>${category.name}</option>`;
return html;
}
function appendChidrenBox(insertHTML) {
let childrenSelectHtml = '';
childrenSelectHtml = `
<div id='children-wrapper'>
<select id='children-category' class='form-control' name='[children_id]'>
<option value='---' data-category='---'>---</option>
${insertHTML}
</select>
<i class='fas fa-chevron-down'></i>
</div>
`;
$('.category-form').append(childrenSelectHtml);
}
function appendGrandchidrenBox(insertHTML) {
let grandchildrenSelectHtml = '';
grandchildrenSelectHtml = `
<div id='grandchildren-wrapper'>
<select id='grandchildren-category' class='form-control' name='[grandchildren_id]'>
<option value='---' data-category='---'>---</option>
${insertHTML}
</select>
<i class='fas fa-chevron-down'></i>
</div>
`;
$('.category-form').append(grandchildrenSelectHtml);
}
$('#parent-category').on('change', function() {
let parentId = document.getElementById('parent-category').value;
if (parentId != '---') {
$.ajax({
url: '/get_category/children',
type: 'GET',
data: {
parent_id: parentId,
},
dataType: 'json',
})
.done(function(children) {
$('#children-wrapper').remove();
$('#grandchildren-wrapper').remove();
let insertHTML = '';
children.forEach(function(children) {
insertHTML += appendOption(children);
});
appendChidrenBox(insertHTML);
})
.fail(function() {
alert('ジャンル取得に失敗しました');
});
} else {
$('#children-wrapper').remove();
$('#grandchildren-wrapper').remove();
}
});
$('.category-form').on('change', '#children-category', function() {
let childrenId = $('#children-category option:selected').data('category');
if (childrenId != '---') {
$.ajax({
url: '/get_category/grandchildren',
type: 'GET',
data: {
children_id: childrenId,
},
dataType: 'json',
})
.done(function(grandchildren) {
if (grandchildren.length != 0) {
$('#grandchildren-wrapper').remove();
let insertHTML = '';
grandchildren.forEach(function(grandchildren) {
insertHTML += appendOption(grandchildren);
});
appendGrandchidrenBox(insertHTML);
}
})
.fail(function() {
alert('ジャンル取得に失敗しました');
});
} else {
$('#grandchildren-wrapper').remove();
}
});
});
【解説】
① セレクトボックスのオプションを設定する。
$(function() {
function appendOption(category) {
let html = `<option value='${category.id}' data-category='${category.id}'>${category.name}</option>`;
return html;
}
◎ パラメーターとして送信する値を設定する。
<option value='${category.id}' data-category='${category.id}'>
※重要ポイント
3.コントローラーを編集
の ①
で受け取るパラメーターに該当する。
◎ セレクトボックスに表示する値を設定する。
${category.name}
② 子ジャンルのセレクトボックスを作成する。
function appendChidrenBox(insertHTML) {
let childrenSelectHtml = '';
childrenSelectHtml = `
<div id='children-wrapper'>
<select id='children-category' class='form-control' name='[children_id]'>
<option value='---' data-category='---'>---</option>
${insertHTML}
</select>
<i class='fas fa-chevron-down'></i>
</div>
`;
$('.category-form').append(childrenSelectHtml);
}
◎ ①
で作成したパラメーターの、パラメーター名
を設定する。
name='[children_id]
◎ ①
で設定したオプションを元に、子カテゴリーのセレクトボックスを作成する。
${insertHTML}
◎ 子カテゴリーのセレクトボックスを表示する。
$('.category-form').append(childrenSelectHtml);
③ 孫ジャンルのセレクトボックスを作成する。( ②
とほぼ同じなので説明は省略)
function appendGrandchidrenBox(insertHTML) {
let grandchildrenSelectHtml = '';
grandchildrenSelectHtml = `
<div id='grandchildren-wrapper'>
<select id='grandchildren-category' class='form-control' name='[grandchildren_id]'>
<option value='---' data-category='---'>---</option>
${insertHTML}
</select>
<i class='fas fa-chevron-down'></i>
</div>
`;
$('.category-form').append(grandchildrenSelectHtml);
}
④ 親カテゴリーが選択された時に発火するイベントを作成する。
$('#parent-category').on('change', function() {
let parentId = document.getElementById('parent-category').value;
if (parentId != '---') {
$.ajax({
url: '/get_category/children',
type: 'GET',
data: {
parent_id: parentId,
},
dataType: 'json',
})
.done(function(children) {
$('#children-wrapper').remove();
$('#grandchildren-wrapper').remove();
let insertHTML = '';
children.forEach(function(children) {
insertHTML += appendOption(children);
});
appendChidrenBox(insertHTML);
})
.fail(function() {
alert('ジャンル取得に失敗しました');
});
} else {
$('#children-wrapper').remove();
$('#grandchildren-wrapper').remove();
}
});
◎ 親カテゴリーが選択された時に発火する。
$('#parent-category').on('change', function() {});
◎ 選択された親カテゴリーのIDを取得し、変数に代入する。
let parentId = document.getElementById('parent-category').value;
◎ 親カテゴリーが初期値でない場合、
パラメーター(parent_id)に先ほど取得した親カテゴリーのIDを設定して、
get_category_children
アクションを非同期で実行する。
if (parentId != '---') {
$.ajax({
url: '/get_category/children',
type: 'GET',
data: {
parent_id: parentId,
},
dataType: 'json',
})
◎ Ajax通信が成功した場合は、子カテゴリーのセレクトボックスを作成する。
また、既に子カテゴリー以下のセレクトボックスが表示されている状態で親カテゴリーが変更された場合は、
子カテゴリー以下のセレクトボックスを削除し、子カテゴリーのセレクトボックスを作成し直す。
.done(function(children) {
$('#children-wrapper').remove();
$('#grandchildren-wrapper').remove();
let insertHTML = '';
children.forEach(function(children) {
insertHTML += appendOption(children);
});
appendChidrenBox(insertHTML);
})
◎ 非同期通信に失敗した場合は、アラートを表示する。
.fail(function() {
alert('ジャンル取得に失敗しました');
});
◎ 親カテゴリーが初期値の場合は、子カテゴリー以下を削除する。
} else {
$('#children-wrapper').remove();
$('#grandchildren-wrapper').remove();
}
④ 子カテゴリーが選択された時に発火するイベントを作成する。( ③
とほぼ同じ)
$('.category-form').on('change', '#children-category', function() {
let childrenId = $('#children-category option:selected').data('category');
if (childrenId != '---') {
$.ajax({
url: '/get_category/grandchildren',
type: 'GET',
data: {
children_id: childrenId,
},
dataType: 'json',
})
.done(function(grandchildren) {
if (grandchildren.length != 0) {
$('#grandchildren-wrapper').remove();
let insertHTML = '';
grandchildren.forEach(function(grandchildren) {
insertHTML += appendOption(grandchildren);
});
appendGrandchidrenBox(insertHTML);
}
})
.fail(function() {
alert('ジャンル取得に失敗しました');
});
} else {
$('#grandchildren-wrapper').remove();
}
});
◎ 孫カテゴリーが無いカテゴリーもあるので条件を付与する。(必要に応じて子カテゴリーにも条件を付与する)
if (grandchildren.length != 0)
完成!!!
ancestory便利ですね!
気合いコード感が半端ないので、気になるところあればコメントでご指摘頂ければと思います。