始めに
オリジナルアプリに投稿を日時やいいねの数によってソートする機能が欲しくて、結構ハマりつつ何とか実装できましたので、備忘録として残しておきます。
前提
●環境
・Ruby 2.6系
・Rails 5.2系
●使用ライブラリ
・Slim
・devise
●いいね機能が実装してあること
・【Rails】いいね機能の実装(この記事でSlimとdeviseの導入も行ってます)
↓実装後はこんな感じです↓
実装
1. まずはプルダウン用のフォームを作成
app/views/posts/index.html.slimh1 投稿一覧 = 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 #================元から実装されている部分========================
・それぞれ何が起きてるか分解して見てみます。
= 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を使用しない場合は必ず付けないと、レンダリングされなくなってしまいます。= form.select :keyword, [ ['投稿が新しい順', 'new'], ['投稿が古い順', 'old'], ['いいねが多い順', 'likes'], ['いいねが少ない順', 'dislikes'], ] = form.submit
・selectメソッドを使用することによってプルダウンフォームを作成することができます。
使い方はselect("object", "method", "choice")
といった感じで使います。・
第一引数であるobject
はpostオブジェクト
に当たりますが、form_withを使う場合は省略してください。・
第二引数であるmethod
はコントローラへパラメータとして送信される
物になるので、自由に記述してください。
この場合、keywordを指定していますので、params[:keyword]
として送信されます。・
第三引数であるchoice
部分では、
[ [プルダウンで表示する値その1, アクションへ送信されるパラメータ],
を指定します。
[プルダウンで表示する値その2, アクションへ送信されるパラメータ],
[プルダウンで表示する値その1, アクションへ送信されるパラメータ] ]
2. ルーティングを編集
config/routes.rbRails.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コントローラを編集
app/controllers/posts_controller.rbclass PostsController < ApplicationController . . . def search selection = params[:keyword] @posts = Post.sort(selection) end . . . end
・searchアクションを作成します。params[:keyword]ではソートフォームで選択した値を受け取って、selectionというローカル変数へ格納してます。
・sort
はPostに対するインスタンスメソッドです、この後Postモデルクラスに、selectionの値によってソートする順番で投稿が取得するように定義します。
4. Postモデルにsortインスタンスメソッドを定義する。
app/models/user.rbclass 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に渡ってきた値によって、ソートする条件を定義しています。
↓日付によるソート
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))
・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. 検索結果のビューを表示
app/views/posts/search.html.slimh1 検索結果 = 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. 検索フォームのパーシャル化
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. 投稿一覧&検索結果一覧のパーシャル化
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. それぞれのビューへレンダリング
app/views/posts/index.html.slimh1 投稿一覧 = render 'select_form' = render 'result'
app/views/posts/index.html.slimh1 検索結果 = render 'select_form' = render 'result'
以上で、完了です。
最後に
以上で、ソート機能実装の完了です!
正直、色んな部分で自信がありませんので、どこか間違っているところなどありましたらおっしゃって頂けるととても嬉しいです!
とっても参考にさせて頂いた記事
gem無しで検索機能を実装するには?複数テーブルにも対応!
【Rails】完全理解 formでセレクトボックスをつくるselectの使い方
select (ActionView::Helpers::FormOptionsHelper) - APIdock