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

# 概要
以下の画像のように、選択したカテゴリーに所属する商品を一覧表示する機能です。
カテゴリーは多階層になっていて、下の階層に行くほど絞り込んだ検索が出来るようになります。
カテゴリーはgemのancestryを用いて作成しています。
![d3a7f46ab307f384f41fe88f3917826b.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619967/295b0c70-83ac-bcf6-f87e-5c683a855121.gif)

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

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

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

#実装
##カテゴリー一覧ページ
はじめに、全てのカテゴリーを表示する一覧ページを作成していきます。
categoriesコントローラーのindexアクションを使用します。
<img width="810" alt="bda976886433b175ea19e2cecab7afa1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619967/8cd0bf51-698b-7573-13b7-e2b434df1e71.png">

###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の値に応じて表示内容を変化させます。

###他カテゴリーへのリンク作成
最後に、以下のように他カテゴリーへのリンクを追加し、より商品を探しやすくします。
<img width="813" alt="4f5519ec96f8863baa48a2f49dd17281.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619967/ae08e45f-26d7-af2a-e6ec-04eb4ecf6c5a.png">
表示カテゴリーが親 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を整えれば完成になります。

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