#はじめに
前回、こちらからカテゴリー機能の実装をまとめてみた。
- 実装した機能は下記の通り。
-
ancestry
によるデータの多階層化 - 非同期通信(Ajax)を用いた動的なセレクトボックスの実装
前回の機能だけではカテゴリーに所属するpostを絞り込み表示する機能がない。
ここでは各カテゴリーに所属する投稿(post)を表示する機能を実装する。
#カテゴリー検索機能の実装
##ルーティング
resources :posts do
collection do
get "menu_search"
end
member do
get "item_search"
end
end
collection内にget "menu_search"
を追加。
member内にget item_search
を追加。
パスは下記の通り。
~/posts/menu_search
~/posts/:id/item_search
menu_search
はカテゴリーのリストを表示するためのリクエスト。
item_search
はカテゴリーリストから選択したカテゴリーに紐づくpostを参照するリクエスト。
##独自アクションの定義
def menu_search
respond_to do |format|
format.html
format.json do
if params[:parent_id]
@childrens = Category.find(params[:parent_id].to_s).children
elsif params[:children_id]
@grandChilds = Category.find(params[:children_id].to_s).children
elsif params[:gcchildren_id]
@parents = Category.where(id: params[:gcchildren_id].to_s)
end
end
end
end
private
マウス操作によってカテゴリーリストを表示させるために必要なインスタンス変数を条件分岐する。
respond_to
でリクエストされるフォーマットがhtmlとjsonの場合で処理を分けている。
format.json
の処理が行われる場合、渡ってきた値によって格納された値の階層下をインスタンス変数に定義する。
params[:xxxxx_id].to_s
はjsで記述したajaxで渡ってくる値を取得する。
children
はancestryが提供するメソッド。子レコードを取得する。
ここのインスタンス変数はmenu_search.json.jbuilder
に渡り、JSON形式へと変換される。
def item_search
#クリックしたカテゴリーidに紐づくレコードを取得
@category = Category.find_by(id: params[:id])
#親カテゴリーが選択された時
if @category.ancestry.nil?
#取得したレコードに紐づいた孫レコードidを取得
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
#取得したレコードに紐づいた子レコードidを取得
category = Category.find_by(id: params[:id]).child_ids
@posts = []
find_item(category)
end
end
def find_item(category)
category.each do |id|
#子・孫レコードのidと一致するpostを取得
post_array = Post.where(category_id: id).order(created_at: :desc)
next unless post_array.present?
post_array.each do |post|
#一致して取得したpostを1つずつ取り出して@postsに順次追加
@posts.push(post) if post.present?
end
end
end
private
条件に一致するカテゴリーのidに紐づいたpostの一覧を取得するアクション。
item_search
では、親or孫orそれ以外(子)を分岐させてカテゴリーidに紐づくpostを取得し、それをインスタンス変数に定義。
find_item(category)
では、item_searchに必要なカテゴリーidに紐づいた空ではないpostを表示させるアクションを定義。
present?
は配列の中に無駄な空の配列を含めないために条件分岐している。
下記はancestry独自のメソッド。
indirect_ids
は、孫レコード以下のIDを返すメソッド。
child_ids
は、子レコードのIDを取得するメソッド。
##jbuilderファイルの作成
touch app/views/posts/menu_search.json.jbuilder
空のファイルを作成。
中身を書いていく。
json.array! @childrens do |child|
json.id child.id
json.name child.name
end
json.array! @grandChilds do |gc|
json.id gc.id
json.name gc.name
json.root gc.root_id
end
json.array! @parents do |parent|
json.parent parent.parent_id
end
前回同様、JSON形式に変換するための記述を行う。
##javascriptファイルの作成
touch app/javascript/packs/category.js
空のファイルを作成。
中身を書いていく。
$(document).on('turbolinks:load', function () {
// 子カテゴリーを表示した時のhtml
function childBuild(children) {
let child_category = `
<li class="category_child">
<a href="/posts/${children.id}/item_search"><input class="child_btn" type="button" value="${children.name}" name= "${children.id}">
</a>
</li>
`;
return child_category;
}
//孫カテゴリーを表示した時のhtml
function gcBuild(children) {
let gc_category = `
<li class="category_grandchild">
#posts/id/item_searchに
<a href="/posts/${children.id}/item_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(); //500ms指定のidを表示
$('.categoryTree').show(); //500ms指定のclassを表示
}, 500);
},
function () {
clearTimeout(timeOut);
}
);
// マウスホバー時に子カテゴリーを表示
$('.parent_btn').hover(
function () {
$('.parent_btn').css('color', ''); //cssの適用を変更
$('.parent_btn').css('background-color', '');
let categoryParent = $(this).attr('name'); //要素(parent_btn)に紐づく属性(name)の値を取得
timeParent = setTimeout(function () {
$.ajax({
url: '/posts/menu_search',
type: 'GET',
data: {
parent_id: categoryParent,
},
dataType: 'json',
})
.done(function (data) {
$('.categoryTree-grandchild').hide(); //孫カテゴリーの表示を隠す
$('.category_child').remove(); //上で記述した子のhtmlを削除
$('.category_grandchild').remove(); //上で記述した孫のhtmlを削除
$('.categoryTree-child').show(); //子カテゴリーを表示
data.forEach(function (child) {
let child_html = childBuild(child);
$('.categoryTree-child').append(child_html); // 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'); //要素(child_btn)に紐づく属性(name)の値を取得
timeChild = setTimeout(function () {
$.ajax({
url: '/posts/menu_search',
type: 'GET',
data: {
children_id: categoryChild,
},
dataType: 'json',
})
.done(function (gc_data) {
$('.category_grandchild').remove(); //上記に記述した孫のhtmlを削除
$('.categoryTree-grandchild').show(); //孫カテゴリーを表示
gc_data.forEach(function (gc) {
let gc_html = gcBuild(gc);
$('.categoryTree-grandchild').append(gc_html); // html要素(孫)を動的に追加
let parcol = $('.categoryTree').find(`input[name="${gc.root}"]`); //classに含まれるinputに紐づいた孫のルートidを取得
$(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'); //要素(document)に紐づく属性(name)の値を取得
timeGc = setTimeout(function () {
$.ajax({
url: '/posts/menu_search',
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 () {
// 何も記述しない
}
);
// カテゴリーを非表示
$(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 () { //マウスが対象の要素の上に重なった時の処理
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'
);
});
リンクにマウスホバーした時の動作を定義。
##javascriptの読み込みを有効化
require('./category');
読み込みにcategory.js
ファイルを指定して有効にする。
##ビュー
touch app/views/posts/_category.html.erb
html.erbファイルをパーシャルで作成。
カテゴリー表示をするビューの部分を個別に持たせる。
<ul class='header__headerInner__nav__listsLeft'>
<li class='header__headerInner__nav__listsLeft__item'>
<%= link_to posts_path, class: "category-button", id: 'categoBtn' do %>
カテゴリーから探す
<% end %>
<div id='tree_menu'>
<ul class='categoryTree'>
<% @set_parents.each do |parent| %>
<li class="category_parent">
<%= link_to item_search_post_path(parent) do %>
<input type="button" value="<%= parent.name %>" name="<%= parent.id %>" class="parent_btn">
<% end %>
</li>
<% end %>
<li class="category_parent">
<%= link_to posts_path do %>
<input type="button" value="全てのカテゴリー" id="all_btn">
<% end %>
</li>
</ul>
<ul class='categoryTree-child'></ul>
<ul class='categoryTree-grandchild'></ul>
</div>
</li>
</ul>
ここの記述から親カテゴリーを表示し、マウスホバーから動的に子・孫カテゴリーをjavascriptで表示させる。
link_to item_search_post_path(parent) do
の記述によってidに基づくリンク先を生成。
input
タグによって、ボタン形式のカテゴリー名とidを格納したリンクが作られる。
touch app/views/posts/item_search.html.erb
先ほど作ったパーシャルを持たせるhtml.erbを作成。
ルーティング先であったitem_search
をビューで作り、ここにカテゴリーを選択した際の絞り込み結果が表示されるようにする。
<%= render "category" %>
<%= render @posts %>
category
は作ったパーシャルを読み込んでいる。
@posts
はitem_searchアクションで取得したpost一覧を表示する。
あとは
<%= render "category" %>
<%= render @posts %>
ヘッダーに埋め込む方が現実的かとは思うが今回はindexにパーシャルを読み込ませる。
これで画面遷移した時に同じカテゴリー機能が表示されることになる。
ここの@posts
はindexアクションにpost.allとした値を取得させる。
indexでは全てのpostをカテゴリーに関係なく一覧表示させる。
def index
@posts = Post.all
end
ここまでCSSを適用していないためサンプルの見た目にはなっていない。
最後にCSSを適用させて終わる。
##CSS
touch app/assets/stylesheets/category.scss
空のscssファイルを作成。
中身を書いていく。
#tree_menu {
width: 200px;
position: absolute;
z-index: 10;
display: flex;
.categoryTree {
display: none;
height: 100%;
border: 1px solid #34aeb3;
background-color: #ffffff;
border-radius: 4px;
box-shadow: 4px 4px 4px #999;
a {
padding: 0;
input[type='button'] {
min-width: 200px;
height: 35px;
font-size: 14px;
text-decoration: none;
color: #3ccace;
background: #ffffff;
outline: none;
transition: 0.4s;
border: #ffffff;
padding: 0 0.9em;
cursor: pointer;
}
input[type='button']:hover {
background: #62d4d7;
color: white;
}
input[type='button']:active {
-webkit-transform: translateY(2px);
transform: translateY(2px);
box-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
background-image: linear-gradient(#b1e9eb 0%, #30a1a4 100%);
}
}
}
.categoryTree-child {
display: none;
height: 100%;
border: 1px solid #34aeb3;
background-color: #ffffff;
border-radius: 4px;
box-shadow: 4px 4px 4px #999;
.category_child {
a {
padding: 0;
input[type='button'] {
min-width: 230px;
height: 35px;
font-size: 14px;
text-decoration: none;
color: #3ccace;
background: #ffffff;
outline: none;
transition: 0.4s;
border: #ffffff;
padding: 0 0.9em;
cursor: pointer;
}
input[type='button']:hover {
background: #62d4d7;
color: white;
}
input[type='button']:active {
-webkit-transform: translateY(2px);
transform: translateY(2px);
box-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
background-image: linear-gradient(#b1e9eb 0%, #30a1a4 100%);
}
}
}
}
.categoryTree-grandchild {
display: none;
height: 100%;
border: 1px solid #34aeb3;
background-color: #ffffff;
border-radius: 4px;
box-shadow: 4px 4px 4px #999;
a {
padding: 0;
input[type='button'] {
min-width: 250px;
height: 35px;
font-size: 14px;
text-decoration: none;
color: #3ccace;
background: #ffffff;
outline: none;
transition: 0.4s;
border: #ffffff;
padding: 0 0.9em;
cursor: pointer;
}
input[type='button']:hover {
border: 1px solid #62d4d7;
background: #62d4d7;
color: white;
}
input[type='button']:active {
-webkit-transform: translateY(2px);
transform: translateY(2px);
box-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
background-image: linear-gradient(#b1e9eb 0%, #30a1a4 100%);
}
}
}
}
#詳細ページに参照したカテゴリーを表示
def show
@post = Post.find(params[:id])
@category_id = @post.category_id
@category_parent = Category.find(@category_id).parent.parent
@category_child = Category.find(@category_id).parent
@category_grandchild = Category.find(@category_id)
end
@post
は特定のpostを参照。
@category_id
はpostに紐づくカテゴリーを参照。
@category_parent
は紐づいたカテゴリーの親の親を参照。
@category_child
は紐づいたカテゴリーの親を参照。
@category_grandchild
は紐づいたカテゴリーを参照。
<%= "#{ @category_parent.name } > #{ @category_child.name } > #{ @category_grandchild.name }" %>
定義したインスタンス変数からカラムを指定して表示させる。
##終わりに
まるまる参考にさせてもらっている状態なので参考にした記事以上には、とても説明できないのが本音です。
参考にさせて頂いた記事を確認して頂くと間違いないかと思います。
重複する記述もあるのでもっとコンパクトにできそうに思えますが力不足でリファクタリングまではできませんでした。
また説明ができないのも自分の理解が追いついていない結果かと思いますので、いずれわかりやすく説明できるように努めたいと思います。
#参考もと
jQueryとancestryを使ったカテゴリー選択機能(Ajax)
【Rails】カテゴリ検索機能の実装で学んだこと