目標
開発環境
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina
前提
下記実装済み。
・Slim導入
・Bootstrap3導入
・Font Awesome導入
・ログイン機能実装
・投稿機能実装
・多対多のカテゴリー機能実装
・多階層カテゴリー機能実装(準備編)
・多階層カテゴリー機能実装(seed編)
実装
1.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
2.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)
3.コントローラーを編集
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
4.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": "税金"
},
]
5.ルーティングを追加
# 追記
get 'get_category/children', to: 'books#get_category_children', defaults: { format: 'json' }
get 'get_category/grandchildren', to: 'books#get_category_grandchildren', defaults: { format: 'json' }
6.ビューを編集
.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'
7.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)
注意
turbolinks
を無効化しないとセレクトボックスが非同期で動作しないので、必ず無効化しておきましょう。