はじめに
アプリ開発において、ancestryというgemを用いてカテゴリー機能を加えたのでまとめました。
目次
- カテゴリー選択・保存
- カテゴリー検索表示
- カテゴリー検索結果表示
1. カテゴリー選択・保存
categoriesテーブルの作成
ancestryをインストールします。
gem 'ancestry'
次にcategoryモデルを作成します。
ターミナル
rails g model category
has_ancestryを記述します。
class Category < ApplicationRecord
has_many :posts
has_ancestry
end
以下のようにマイグレーションファイルに記述します。
indexについてはこちら
class CreateCategories < ActiveRecord::Migration[6.0]
def change
create_table :categories do |t|
t.string :name, index: true, null: false
t.string :ancestry, index: true
t.timestamps
end
end
end
googleスプレッドシートにカテゴリーを記述していきます。
Aの列がid、Bの列がname(カテゴリー名)、Cの列がancestry(親子孫を見分ける数値)となります。
データの保存方法は、ファイル → ダウンロード → カンマ区切りの値(.csv 現在のシート) の手順で保存できます。
ダウンロードしたcsvファイルはdbフォルダに配置します。
seeds.rbファイル内へ以下の通り記述します。
require "csv"
CSV.foreach('db/category.csv') do |row|
Category.create(:id => row[0], :name => row[1], :ancestry => row[2])
end
ターミナルでrails db:seedコマンドを実行するとcsvファイルを読み込み自動でDBのレコードが生成されます。
foreachの後に読み込みたいファイルの指定を行います。
その下の記述については、モデル名.create(カラム名 => 読み込みたい列)となります。
row[0] → Aの列がid
row[1] → Bの列がname(カテゴリー名)
row[2] → Cの列がancestry(親子孫を見分ける数値)
ルーティング
子、孫カテゴリーをjson形式でルーティングを設定します。
Rails.application.routes.draw do
~略~
resources :posts do
collection do
get 'top'
get 'get_category_children', defaults: { format: 'json' }
get 'get_category_grandchildren', defaults: { format: 'json' }
get 'name_search'
end
~略~
end
コントローラー
postsコントローラーに親カテゴリーを定義します。
複数箇所で使用するためbefore_actionを使って定義します。
def set_parents
@parents = Category.where(ancestry: nil)
end
postsコントローラーに子、孫カテゴリーのメソッドを定義します。
def get_category_children
@category_children = Category.find("#{params[:parent_id]}").children
end
def get_category_grandchildren
@category_grandchildren = Category.find("#{params[:child_id]}").children
end
json.jbuilderファイルを作成し、jsonデータへ変換します。
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
ビュー
javascriptでカテゴリー選択時の動作を設定します。
:app/javascript/category_post.js
$(function(){
function appendOption(category){
var html = `<option value="${category.id}">${category.name}</option>`;
return html;
}
function appendChildrenBox(insertHTML){
var childSelectHtml = "";
childSelectHtml = `<div class="category__child" id="children_wrapper">
<select id="child__category" name="post[category_id]" class="serect_field">
<option value="">---</option>
${insertHTML}
</select>
</div>`;
$('.append__category').append(childSelectHtml);
}
function appendGrandchildrenBox(insertHTML){
var grandchildSelectHtml = "";
grandchildSelectHtml = `<div class="category__child" id="grandchildren_wrapper">
<select id="grandchild__category" name="post[category_id]" class="serect_field">
<option value="">---</option>
${insertHTML}
</select>
</div>`;
$('.append__category').append(grandchildSelectHtml);
}
$('#item_category_id').on('change',function(){
var parentId = document.getElementById('item_category_id').value;
if (parentId != ""){
$.ajax({
url: '/posts/get_category_children/',
type: 'GET',
data: { parent_id: parentId },
dataType: 'json'
})
.done(function(children){
$('#children_wrapper').remove();
$('#grandchildren_wrapper').remove();
var insertHTML = '';
children.forEach(function(child){
insertHTML += appendOption(child);
});
appendChildrenBox(insertHTML);
if (insertHTML == "") {
$('#children_wrapper').remove();
}
})
.fail(function(){
alert('カテゴリー取得に失敗しました');
})
}else{
$('#children_wrapper').remove();
$('#grandchildren_wrapper').remove();
}
});
$('.append__category').on('change','#child__category',function(){
var childId = document.getElementById('child__category').value;
if(childId != ""){
$.ajax({
url: '/posts/get_category_grandchildren',
type: 'GET',
data: { child_id: childId },
dataType: 'json'
})
.done(function(grandchildren){
$('#grandchildren_wrapper').remove();
var insertHTML = '';
grandchildren.forEach(function(grandchild){
insertHTML += appendOption(grandchild);
});
appendGrandchildrenBox(insertHTML);
if (insertHTML == "") {
$('#grandchildren_wrapper').remove();
}
})
.fail(function(){
alert('カテゴリー取得に失敗しました');
})
}else{
$('#grandchildren_wrapper').remove();
}
})
});
新規投稿ページにカテゴリーセレクトボックスを表示させます。
<div class="append__category">
<div class="category">
<div class="form__label">
<div class="weight-bold-text lavel__name ">
カテゴリー
</div>
<div class="lavel__Required">
<%= f.collection_select :category_id, @parents, :id, :name,{ include_blank: "選択してください"},class:"serect_field", id:"item_category_id" %>
</div>
</div>
</div>
</div>
2. カテゴリー検索表示
コントローラー
def top
respond_to do |format|
format.html
format.json do
if params[:parent_id]
@childrens = Category.find(params[:parent_id]).children
elsif params[:children_id]
@grandChilds = Category.find(params[:children_id]).children
elsif params[:gcchildren_id]
@parents = Category.where(id: params[:gcchildren_id])
end
end
end
end
ビュー
javascriptでどの親カテゴリーにの上にマウスがいるのか、それに属する子カテゴリーや孫カテゴリーを取得しています。
:app/javascript/category.js
$(document).ready(function () {
// 親カテゴリーを表示
$('#categoBtn').hover(function (e) {
e.preventDefault();
e.stopPropagation();
$('#tree_menu').show();
$('.categoryTree').show();
}, function () {
// あえて何も記述しない
});
// 非同期にてヘッダーのカテゴリーを表示
function childBuild(children) {
let child_category = `
<li class="category_child">
<a href="/posts/${children.id}/search"><input class="child_btn" type="button" value="${children.name}" name= "${children.id}">
</a>
</li>
`
return child_category;
}
function gcBuild(children) {
let gc_category = `
<li class="category_grandchild">
<a href="/posts/${children.id}/search"><input class="gc_btn" type="button" value="${children.name}" name= "${children.id}">
</a>
</li>
`
return gc_category;
}
// 親カテゴリーを表示
$('#categoBtn').hover(function (e) {
e.preventDefault();
e.stopPropagation();
timeOut = setTimeout(function () {
$('#tree_menu').show();
$('.categoryTree').show();
}, 500)
}, function () {
clearTimeout(timeOut)
});
// 子カテゴリーを表示
$('.parent_btn').hover(function () {
$('.parent_btn').css('color', '');
$('.parent_btn').css('background-color', '');
let categoryParent = $(this).attr('name');
timeParent = setTimeout(function () {
$.ajax({
url: '/posts/top',
type: 'GET',
data: {
parent_id: categoryParent
},
dataType: 'json'
})
.done(function (data) {
$(".categoryTree-grandchild").hide();
$(".category_child").remove();
$(".category_grandchild").remove();
$('.categoryTree-child').show();
data.forEach(function (child) {
let child_html = childBuild(child)
$(".categoryTree-child").append(child_html);
});
$('#tree_menu').css('max-height', '490px');
})
.fail(function () {
alert("カテゴリーを選択してください");
});
}, 400)
}, function () {
clearTimeout(timeParent);
});
// 孫カテゴリーを表示
$(document).on({
mouseenter: function () {
$('.child_btn').css('color', '');
$('.child_btn').css('background-color', '');
let categoryChild = $(this).attr('name');
timeChild = setTimeout(function () {
$.ajax({
url: '/posts/top',
type: 'GET',
data: {
children_id: categoryChild
},
dataType: 'json'
})
.done(function (gc_data) {
$(".category_grandchild").remove();
$('.categoryTree-grandchild').show();
gc_data.forEach(function (gc) {
let gc_html = gcBuild(gc)
$(".categoryTree-grandchild").append(gc_html);
let parcol = $('.categoryTree').find(`input[name="${gc.root}"]`);
$(parcol).css('color', 'white');
$(parcol).css('background-color', '#b1e9eb');
});
$('#tree_menu').css('max-height', '490px');
})
.fail(function () {
alert("カテゴリーを選択してください");
});
}, 400)
},
mouseleave: function () {
clearTimeout(timeChild);
}
}, '.child_btn');
// 孫カテゴリーを選択時
$(document).on({
mouseenter: function () {
let categoryGc = $(this).attr('name');
timeGc = setTimeout(function () {
$.ajax({
url: '/posts/top',
type: 'GET',
data: {
gcchildren_id: categoryGc
},
dataType: 'json'
})
.done(function (gc_result) {
let childcol = $('.categoryTree-child').find(`input[name="${gc_result[0].parent}"]`);
$(childcol).css('color', 'white');
$(childcol).css('background-color', '#b1e9eb');
$('#tree_menu').css('max-height', '490px');
})
.fail(function () {
alert("カテゴリーを選択してください");
});
}, 400)
},
mouseleave: function () {
clearTimeout(timeGc);
}
}, '.gc_btn');
// カテゴリー一覧ページのボタン
$('#all_btn').hover(function (e) {
e.preventDefault();
e.stopPropagation();
$(".categoryTree-grandchild").hide();
$(".categoryTree-child").hide();
$(".category_grandchild").remove();
$(".category_child").remove();
}, function () {
// あえて何も記述しないことで親要素に外れた際のアクションだけを伝搬する
});
// カテゴリーを非表示(カテゴリーメニュから0.8秒以上カーソルを外したら消える)
$(document).on({
mouseleave: function (e) {
e.stopPropagation();
e.preventDefault();
timeChosed = setTimeout(function () {
$(".categoryTree-grandchild").hide();
$(".categoryTree-child").hide();
$(".categoryTree").hide();
$(this).hide();
$('.parent_btn').css('color', '');
$('.parent_btn').css('background-color', '');
$(".category_child").remove();
$(".category_grandchild").remove();
}, 800);
},
mouseenter: function () {
timeChosed = setTimeout(function () {
$(".categoryTree-grandchild").hide();
$(".categoryTree-child").hide();
$(".categoryTree").hide();
$(this).hide();
$('.parent_btn').css('color', '');
$('.parent_btn').css('background-color', '');
$(".category_child").remove();
$(".category_grandchild").remove();
}, 800);
clearTimeout(timeChosed);
}
}, '#tree_menu');
// カテゴリーボタンの処理
$(document).on({
mouseenter: function (e) {
e.stopPropagation();
e.preventDefault();
timeOpened = setTimeout(function () {
$('#tree_menu').show();
$('.categoryTree').show();
}, 500);
},
mouseleave: function (e) {
e.stopPropagation();
e.preventDefault();
clearTimeout(timeOpened);
$(".categoryTree-grandchild").hide();
$(".categoryTree-child").hide();
$(".categoryTree").hide();
$("#tree_menu").hide();
$(".category_child").remove();
$(".category_grandchild").remove();
}
}, '.header__headerInner__nav__listsLeft__item');
});
トップ画面にカテゴリー選択ウィンドウをセットします。
<div class="item-categories">
<h2>
カテゴリー一覧
</h2>
<%= link_to posts_path, class: "category-button", id: 'categoBtn' do %>
カテゴリーから探す
<% end %>
<div id="tree_menu">
<ul class="categoryTree">
<% @parents.each do |parent| %>
<li class="category_parent">
<%= link_to search_post_path(parent) do %>
<input type="button" value="<%= parent.name %>" name="<%= parent.id %>" class="parent_btn">
<% end %>
</li>
<% end %>
</ul>
<ul class="categoryTree-child">
</ul>
<ul class="categoryTree-grandchild">
</ul>
</div>
</div>
3. カテゴリー検索結果表示
ルーティング
カテゴリーをidで区別するため、memberを用いてsearchアクションを定義しています。
resources :posts do
~略~
member do
get 'search'
end
~略~
end
コントローラー
クリックしたカテゴリーが、親カテゴリー、子カテゴリー、孫カテゴリーのどれなのかで条件分岐しています。
def search
@category = Category.find_by(id: params[:id])
if @category.ancestry == nil
category = Category.find_by(id: params[:id]).indirect_ids
if category.empty?
@posts = Post.where(category_id: @category.id).order(created_at: :desc)
else
@posts = []
find_item(category)
end
elsif @category.ancestry.include?("/")
@posts = Post.where(category_id: params[:id]).order(created_at: :desc)
else
category = Category.find_by(id: params[:id]).child_ids
@posts = []
find_item(category)
end
end
def find_item(category)
category.each do |id|
post_array = Post.where(category_id: id).order(created_at: :desc)
if post_array.present?
post_array.each do |post|
if post.present?
@posts.push(post)
end
end
end
end
end
ビュー
<div class="item-categories">
<h2>
カテゴリー一覧
</h2>
<%= link_to posts_path, class: "category-button", id: 'categoBtn' do %>
カテゴリーから探す
<% end %>
<div id="tree_menu">
<ul class="categoryTree">
<% @parents.each do |parent| %>
<li class="category_parent">
<%= link_to search_post_path(parent) do %>
<input type="button" value="<%= parent.name %>" name="<%= parent.id %>" class="parent_btn">
<% end %>
</li>
<% end %>
</ul>
<ul class="categoryTree-child">
</ul>
<ul class="categoryTree-grandchild">
</ul>
</div>
</div>
参考リンク
https://qiita.com/k_suke_ja/items/aee192b5174402b6e8ca
https://qiita.com/Sobue-Yuki/items/9c1b05a66ce6020ff8c1
https://qiita.com/dr_tensyo/items/88e8ddf0f5ce37040dc8
https://qiita.com/ATORA1992/items/bd824f5097caeee09678
https://qiita.com/misioro_missie/items/175af1f1678e76e59dea
https://qiita.com/Rubyist_SOTA/items/49383aa7f60c42141871