【Rails】投稿一覧をプルダウンボックスを使い、日付やいいね数でソートする

## 始めに
オリジナルアプリに投稿を日時やいいねの数によってソートする機能が欲しくて、結構ハマりつつ何とか実装できましたので、備忘録として残しておきます。

## 前提
__●環境__
　・Ruby 2.6系
　・Rails 5.2系
__●使用ライブラリ__
　・Slim
　・devise
__●いいね機能が実装してあること__
　・[【Rails】いいね機能の実装](https://qiita.com/kurawo___D/items/d8115ecae71164a70f18)（この記事でSlimとdeviseの導入も行ってます）


####↓実装後はこんな感じです↓
（見栄え悪くてゴメンナサイ…）
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606750/a5639a16-6d5b-c0fc-5034-6bc12e1ff1e7.gif)

## 実装

### 1.　まずはプルダウン用のフォームを作成

>```ruby:app/views/posts/index.html.slim
h1 投稿一覧
　
= form_with model: @post, url: search_path, method: :get, local: true do |form|
  = form.select :keyword, [ ['投稿が新しい順', 'new'],
                            ['投稿が古い順', 'old'],
                            ['いいねが多い順', 'likes'],
                            ['いいねが少ない順', 'dislikes'],
                          ]
  = form.submit
　
#================元から実装されている部分========================
- @posts.each do |post|
  p
    = link_to post.user.email, user_path(post.user.id)
    br
    = link_to post.title, post_path(post.id)
    br
    = post.created_at
    br
    - if user_signed_in?
      - if post.favorited_by?(current_user)
        = link_to "いいね: #{post.favorites.count}", post_favorites_path(post.id), method: :delete
      - else
        = link_to "いいね: #{post.favorites.count}", post_favorites_path(post.id), method: :post
    - else
      = post.favorites.count
#================元から実装されている部分========================
```


>>__・それぞれ何が起きてるか分解して見てみます。__

>>```ruby
= form_with model: @post, url: search_path, 
            method: :get, local: true do |form|
```

>>__・フォームで使用するモデルとして```Postモデル```を指定してます。これにより`params[:post]`という値でデータが送信されます。__

>>__・検索後は検索結果表示用のページを用意するつもりので、パスは```search_path```して、GETリクエストとして送信します。__

>>__・```local: true```。form_withではこれを指定しないとAjax通信と見なされます。JSを使用しない場合は必ず付けないと、レンダリングされなくなってしまいます。__

>>```ruby
= form.select :keyword, [ ['投稿が新しい順', 'new'],
                          ['投稿が古い順', 'old'],
                          ['いいねが多い順', 'likes'],
                          ['いいねが少ない順', 'dislikes'],
                        ]
= form.submit
```


>>__・selectメソッドを使用することによってプルダウンフォームを作成することができます。
　使い方は```select("object", "method", "choice")```といった感じで使います。__

>>__・```第一引数であるobject```は```postオブジェクト```に当たりますが、form_withを使う場合は省略してください。__

>>__・```第二引数であるmethod```は```コントローラへパラメータとして送信される```物になるので、自由に記述してください。__
　__この場合、keywordを指定していますので、```params[:keyword]```として送信されます。__

>>__・```第三引数であるchoice```部分では、
　```[ [プルダウンで表示する値その１, アクションへ送信されるパラメータ], 
　[プルダウンで表示する値その2, アクションへ送信されるパラメータ], 
　[プルダウンで表示する値その１, アクションへ送信されるパラメータ] ]```を指定します。__

### 2.　ルーティングを編集

>```ruby:config/routes.rb
Rails.application.routes.draw do
#================元から実装されている部分========================
  devise_for :users
  resources :users
　
  root to: 'posts#index'
  resources :posts, only: [:index, :new, :create, :show, :edit, :update] do
    resource :favorites, only: [:create, :destroy]
  end
#================元から実装されている部分========================
　
  #検索結果をpostsコントローラのsearchアクションへ送信
+ get 'search' => 'posts#search'
　
end
```

### 3.　postsコントローラを編集

>```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
  .
  .
  .
 def search
    selection = params[:keyword]
    @posts = Post.sort(selection)
 end
  .
  .
  .
end
```

>>__・searchアクションを作成します。params[:keyword]ではソートフォームで選択した値を受け取って、selectionというローカル変数へ格納してます。__
　
>>__・`sort`はPostに対するインスタンスメソッドです、この後Postモデルクラスに、selectionの値によってソートする順番で投稿が取得するように定義します。__

### 4.　Postモデルにsortインスタンスメソッドを定義する。

>```ruby:app/models/user.rb
class Post < ApplicationRecord
  .
  .
  .
  def self.sort(selection)
    case selection
    when 'new'
      return all.order(created_at: :DESC)
    when 'old'
      return all.order(created_at: :ASC)
    when 'likes'
      return find(Favorite.group(:post_id).order(Arel.sql('count(post_id) desc')).pluck(:post_id))
    when 'dislikes'
      return find(Favorite.group(:post_id).order(Arel.sql('count(post_id) asc')).pluck(:post_id))
    end
  end
  .
  .
  .
end
```

>>__・インスタンスメソッドの定義なのでselfを付与してます。__

>>__・case文によって、selectionに渡ってきた値によって、ソートする条件を定義しています。__

>>__↓日付によるソート__

>>```ruby
when 'new'
   return all.order(created_at: :DESC)
when 'old'
   return all.order(created_at: :ASC)
```

>>__↓いいね数によるソート__

>>```ruby
when 'likes'
   return find(Favorite.group(:post_id).order(Arel.sql('count(post_id) desc')).pluck(:post_id))
when 'dislikes'
   return find(Favorite.group(:post_id).order(Arel.sql('count(post_id) asc')).pluck(:post_id))
```

>>__・Favorite.group(:post_id)で、`post_idが同じ投稿をグループ分け`します。__

>>__・order(Arel.sql('count(post_id) desc'))で、`グループ分けされた投稿へどれだけuser_idが格納されているかをカウントして多いものから並べます`（つまりどれだけいいねされているかを集計してます）。__
　__`Arel.sql()はSQLインジェクション対策`です。__

>>__・pluck(:post_id)では、`post_idそのものの取得`を行っています。これを記述しないと、Postへ対して呼び出しているfindに該当するものが見つからず、エラーになってしまいます。__

_※いいねの数が0の投稿が表示されない問題に関しては、現状では未対応です。申し訳ありません…。解決出来次第追記します。_

### 5.　検索結果のビューを表示

>```ruby:app/views/posts/search.html.slim
h1 検索結果
　
= form_with url: search_path, method: :get, local: true do |form|
  = form.select :keyword, [ ['投稿が新しい順', 'new'],
                            ['投稿が古い順', 'old'],
                            ['いいねが多い順', 'likes'],
                            ['いいねが少ない順', 'dislikes'],
                          ]
  = form.submit
　
- @posts.each do |post|
  p
    = link_to post.user.email, user_path(post.user.id)
    br
    = link_to post.title, post_path(post.id)
    br
    = post.created_at
    br
    - if user_signed_in?
      - if post.favorited_by?(current_user)
        = link_to "いいね: #{post.favorites.count}", post_favorites_path(post.id), method: :delete
      - else
        = link_to "いいね: #{post.favorites.count}", post_favorites_path(post.id), method: :post
    - else
      = post.favorites.count
```

>>__・app/views/postsディレクトリ内に、seatch.html.slimファイルを作成して、上記のように記述します。__
　
>>__・ここに記述されている内容は、posts/index.html.slimに記述されている内容と全く同じ内容なので、パーシャル化してしまいます。__

### 6.　ビューのパーシャル化

>#### __6-1. 検索フォームのパーシャル化__

>```ruby:app/views/posts/_select_form.html.slim
= form_with model: @post, url: search_path, method: :get, local: true do |form|
  = form.select :keyword, [ ['投稿が新しい順', 'new'],
                            ['投稿が古い順', 'old'],
                            ['いいねが多い順', 'likes'],
                            ['いいねが少ない順', 'dislikes'],
                          ]
  = form.submit
```

>>__・app/views/postsディレクトリ内に、`_select_form.html.slim`ファイルを作成して、プルダウンメニューのコードを丸ごと移植します。__

>#### __6-2. 投稿一覧＆検索結果一覧のパーシャル化__

>```ruby:app/views/posts/_result.html.slim
- @posts.each do |post|
  p
    = link_to post.user.email, user_path(post.user.id)
    br
    = link_to post.title, post_path(post.id)
    br
    = post.created_at
    br
    - if user_signed_in?
      - if post.favorited_by?(current_user)
        = link_to "いいね: #{post.favorites.count}", post_favorites_path(post.id), method: :delete
      - else
        = link_to "いいね: #{post.favorites.count}", post_favorites_path(post.id), method: :post
    - else
      = post.favorites.count
```

>>__・app/views/postsディレクトリ内に、`_result.html.slim`ファイルを作成して、投稿一覧表示用のコードを丸ごと移植します。__

>#### __6-3. それぞれのビューへレンダリング__

>```ruby:app/views/posts/index.html.slim
h1 投稿一覧
　
= render 'select_form'
　
= render 'result'
```

>```ruby:app/views/posts/index.html.slim
h1 検索結果
　
= render 'select_form'
　
= render 'result'
```

#### 以上で、完了です。


##最後に
以上で、ソート機能実装の完了です！
正直、色んな部分で自信がありませんので、どこか間違っているところなどありましたらおっしゃって頂けるととても嬉しいです！

##とっても参考にさせて頂いた記事
[gem無しで検索機能を実装するには？複数テーブルにも対応！](https://prokyou.com/rails/nogemsearch/)
[【Rails】完全理解 formでセレクトボックスをつくるselectの使い方](https://310nae.com/rails-selectbox/)
[select (ActionView::Helpers::FormOptionsHelper) - APIdock](https://apidock.com/rails/ActionView/Helpers/FormOptionsHelper/select)