[Rails]多階層カテゴリーから商品を検索・一覧表示する機能


概要

以下の画像のように、選択したカテゴリーに所属する商品を一覧表示する機能です。
カテゴリーは多階層になっていて、下の階層に行くほど絞り込んだ検索が出来るようになります。
カテゴリーはgemのancestryを用いて作成しています。

前提

・ancestryを用いてカテゴリーテーブルを作成している。
・カテゴリー:商品 = 1:多の関係になっている。
・商品モデルのcategory_idには、最下層のカテゴリーidが登録されている。

この記事のアプリでは3階層のカテゴリーを使用しています。
また、この記事内では一番上のカテゴリーを「親カテゴリー」、その下の階層を「子カテゴリー」、
そのまた下を「孫カテゴリー」と表現しています。

参考記事

ancestryの導入方法については以下の記事が参考になります。
https://qiita.com/Sotq_17/items/120256209993fb05ebac
https://qiita.com/pdm21/items/fe0055b3190af790f1c0

実装

カテゴリー一覧ページ

はじめに、全てのカテゴリーを表示する一覧ページを作成していきます。
categoriesコントローラーのindexアクションを使用します。

indexアクション定義

app/controllers/categories_controller.rb
def index
  @parents = Category.where(ancestry: nil)
end

親カテゴリーのancestryカラムはnilのため、上の記述で親カテゴリーを取得出来ます。

ビュー作成

app/views/categories/index.html.haml
%h1 カテゴリー一覧
%ul.categories
  - @parents.each do |parent|
    %li.parents= link_to "#{parent.name}", category_path(parent)

    %ul.categories__children
      - parent.children.each do |child|
        %li.childern= link_to "#{child.name}", category_path(child)

        %ul.categories__grandchildren
          - child.children.each do |grandchild|
            %li= link_to "#{grandchild.name}", category_path(grandchild)

これで全てのカテゴリーを一覧表示出来ます。
それぞれのカテゴリー名は、後述するカテゴリー別商品一覧ページにリンクしています。

カテゴリー別 商品一覧ページ

続いて、選択したカテゴリーに所属する商品を一覧表示するページを作成していきます。
categoriesコントローラーのshowアクションを使用します。

showアクション定義

app/controllers/categories_controller.rb
before_action :set_category, only: :show

def show
  @items = @category.set_items
  @items = @items.where(buyer_id: nil).order("created_at DESC").page(params[:page]).per(9)
end

private
def set_category
  @category = Category.find(params[:id])
end

後述するモデルメソッド set_itemsにより、カテゴリー内の商品を取得します。

モデルメソッド定義

app/models/category.rb
has_many :items
has_ancestry

def set_items
  # 親カテゴリーの場合
  if self.root?
    start_id = self.indirects.first.id
    end_id = self.indirects.last.id
    items = Item.where(category_id: start_id..end_id)
    return items

    # 子カテゴリーの場合
  elsif self.has_children?
    start_id = self.children.first.id
    end_id = self.children.last.id
    items = Item.where(category_id: start_id..end_id)
    return items

    # 孫カテゴリーの場合
  else
    return self.items
  end
end

商品のcategory_idには孫カテゴリーのidが登録されています。
そのため、単純に @items = @category.items と記述するだけでは、@categoryが孫の場合しか商品情報を取得出来ません。
よって、上記のようにカテゴリーが親 or 子 or 孫の内のどれに当たるかで条件分岐します。
カテゴリーが親 or 子の場合は、自身が持つ孫カテゴリーのid範囲を指定して商品を取得しています。

root?やindirectsなど、ancestry独自のメソッドを使用する際は以下の記事が参考になります。
https://qiita.com/Rubyist_SOTA/items/49383aa7f60c42141871

ビュー作成

app/views/categories/show.html.haml
.products-container
  .products-index
    .title
      = "#{@category.name}の商品一覧"
      .title__border
    - if @items
      %ul.lists
        = render "items/item", items: @items
    = paginate @items

全ての商品一覧ページで使用している部分テンプレートを共用。
@itemsの値に応じて表示内容を変化させます。

他カテゴリーへのリンク作成

最後に、以下のように他カテゴリーへのリンクを追加し、より商品を探しやすくします。

表示カテゴリーが親 or 子の場合、一階層下のカテゴリーへのリンクを、
孫の場合は、同じ子カテゴリーに所属する孫カテゴリーへのリンクを表示するようにします。

app/controllers/categories_controller.rb
def set_category
  @category = Category.find(params[:id])
  # 追記---------------------------------
  if @category.has_children?
    @category_links = @category.children
  else
    @category_links = @category.siblings
  end
  # -------------------------------------
end
app/views/categories/show.html.haml

// 追記--------------------------------------------------
.category_wrapper
  .category_links
    - @category_links.each do |category|
      = link_to category.name, category_path(category)
// -----------------------------------------------------
.products-container
  .products-index
    .title
      = "#{@category.name}の商品一覧"
      .title__border
    - if @items
      %ul.lists
        = render "items/item", items: @items
    = paginate @items

あとは適宜CSSを整えれば完成になります。

最後に

以上で多階層カテゴリーから商品を検索する機能は完成です。
ここまで読んでいただき、ありがとうございました。