🦓

[Rails]タグ 2/2

に公開

はじめに

ヘッダーの検索フォームからタグを検索できるように実装していきます。
また、タグをクリックすると、そのタグが付けられた投稿を絞り込んで表示させていきます。

環境:

Rails 6.1.7.3
ruby 3.0.0

検索用のコントローラーを作成する

Articlesコントローラーにてsearchアクションを作成しましたが、検索用のSearchコントローラーに移します。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
-  def search
-     @q = Article.ransack(params[:q])
-     @articles = @q.result(distinct: true).includes(:user).page(params[:page]).per(20)
-  end
end
app/controllers/search_controller.rb
class SearchController < ApplicationController
    def search
      @q = params[:q]
  
      @articles = Article.ransack(title_or_body_cont: @q).result(distinct: true).includes(:user).page(params[:page]).per(20)
      @tags = Tag.ransack(name_cont: @q).result(distinct: true)
    end
end 

routes.rbを編集する

サーチのURLを変更します。

config/routes.rb
Rails.application.routes.draw do
+ get '/search', to: 'search#search', as: :search
  resources :articles do
      collection do
-        get 'search', to: 'articles#search', as: :search
      end
  end
end
search GET    /search(.:format)     search#search

検索するカラムと関連付けのモデルを追加する

app/models/tag.model
class Tag < ApplicationRecord
    def self.ransackable_attributes(auth_object = nil)
        ["name"]
    end

    def self.ransackable_associations(auth_object = nil)
        %w[article_tags articles]
    end
end
app/models/article.model
class Article < ApplicationRecord
    def self.ransackable_attributes(auth_object = nil)
        %w[title body]
    end

    def self.ransackable_associations(auth_object = nil)
        %w[article_tags tags]
    end
end

検索URLを設定する

ヘッダーにある検索フォームのURLを変えます。

app/views/shared/_header.html.erb
<li class="nav-item">
   <%= render 'shared/search_form', url: search_path %>
</li>

検索パーシャルを作成する

search_form_forを使ってましたが、検索オブジェクトを複数にもあるため、form_withを使用しURLオプションを指定します。

app/views/shared/_search_form.html.erb
<%= form_with url: url, method: :get do |form| %>
  <div class="input-group">
    <%= form.text_field :q, class: 'form-control', placeholder: t('defaults.search_word') %>
    <%= form.submit t('defaults.search_word'), class: 'btn btn-primary' %>
  </div>
<% end %>


URLが短くなりましたね。

form_withで作成された検索フォームです。

検索結果用ビューを作成する

articles/search.html.erbでしたが、Searchコントローラーを作成したため、search/search.html.erbに移動します。

app/views/search/search.html.erb
<div class="row">
  <% if @articles.present? %>
    <h2><%= params[:q] if params[:q].present? %>」の投稿(<%= @articles.length %></h2>
    <div class="row row-cols-1 row-cols-md-3 g-4">
      <% @articles.each do |article| %>
        <%= render 'articles/article', article: article %>
      <% end %>
      <div>
        <%= paginate @articles %>
      </div>
    <% else %>
      <h2>一致する投稿がありませんでした。</h2>
    <% end %>
  </div>
  <% if @tags.present? %>
    <h2><%= params[:q] if params[:q].present? %>」のタグ(<%= @tags.length %></h2>
    <div class="row row-cols-1 row-cols-md-6 g-4">
      <% @tags.each do |tag| %>
        <span class="badge rounded-pill text-bg-dark me-2"><%= tag.name %></span>
      <% end %>
    <% else %>
      <h2>一致するタグがありませんでした。</h2>
    <% end %>
  </div>
</div>

タグ絞り込み検索

タグでの投稿の絞り込み検索を実装していきます。
Articlesモデルにタグスコープを使う方法とTagコントローラーを使う方法両方書いてみました。

スコープとは

scopeメソッドは、モデルクラス内でクエリを定義するために使用されます。

Userモデルがあるとします。このモデルにはnameadminという2つの属性があります。nameは文字列型で、adminは真偽値型です。

class User < ApplicationRecord
  scope :admins, -> { where(admin: true) }
  scope :name_starts_with, ->(prefix) { where("name LIKE ?", "#{prefix}%") }
end

上記のコードでは、2つの異なるスコープが定義されています。

  1. adminsスコープは、admin属性がtrueであるユーザーをフィルタリングします。

  2. name_starts_withスコープは、name属性が指定されたプレフィックスで始まるユーザーをフィルタリングします。このスコープでは、引数としてprefixを取り、SQLのLIKE演算子を使用してデータベースクエリを構築しています。

これらのスコープを使用することで、簡潔なクエリを実行することができます。以下は、これらのスコープを使用した例です。

# adminsスコープを使用して管理者ユーザーを取得する例
admins = User.admins

# name_starts_withスコープを使用して"John"で始まるユーザーを取得する例
john_users = User.name_starts_with("John")

adminsスコープはUser.adminsとして呼び出され、管理者ユーザーのコレクションを返します。name_starts_withスコープはUser.name_starts_with("John")として呼び出され、名前が"John"で始まるユーザーのコレクションを返します。

scopeメソッドにより、再利用可能なクエリの定義が可能になり、コードの可読性と保守性が向上します。

タグスコープを作成する

投稿モデルにタグスコープを作成します。

app/models/article.rb
class Article < ActiveRecord
    scope :with_tag, ->(tag_name) { joins(:tags).where(tags: {name: tag_name}) }

コントローラーでスコープを使う

Articlesコントローラーでスコープを使った投稿一覧を取得できるようにします。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    articles = if (tag_name = params[:tag_name])
                 Article.with_tag(tag_name)
               else
                 Article.all
               end
    @articles = articles.all.includes(:user).order(created_at: :desc).page(params[:page]).per(10)
  end
end

タグパーシャルにリンクを追加する

app/views/articles/_tags.html.erb
<% article.tags.each do |tag| %>
  <%= link_to articles_path(tag_name: tag.name) do %>
    <span class="badge rounded-pill text-bg-dark"><%= tag.name %></span>
  <% end %>
<% end %>

スコープを使ってタグでの投稿の絞り込みを実装しました。
こちらの方法ではタグの独自のビューがないのとタグ関連の機能を増やしたい時にTagコントローラーでロジックをまとめたいため以下の方法でもう一度実装してみました。


routes.rbを編集する

config/routes.rb
Rails.application.routes.draw do
    resources :tags, only: %i[index show]
end

Tagコントローラーを作成する

app/controllers/tags_controller.rb
class TagsController < ApplicationController
    def index
        @tags = Tag.all
    end

    def show
        @tag = Tag.find(params[:id])
    end
end

タグにリンクを追加する

app/views/articles/_tags.html.erb
<% article.tags.each do |tag| %>
  <%= link_to tag_path(tag) do %>
    <span class="badge rounded-pill text-bg-dark"><%= tag.name %></span>
  <% end %>
<% end %>

タグ詳細のビューを作成する

app/views/tags/show.html.erb
<%= content_for(:title, @tag.name) %>
<h1>タグ:<%= @tag.name %>(<%= @tag.articles.size %>)</h1>
<div class="row row-cols-1 row-cols-md-3 g-4">
  <% @tag.articles.each do |article| %>
    <%= render 'articles/article', article: article %>
  <% end %>
</div>

Image from Gyazo

終わりに

gemを使わずにタグ機能を実装してみました。
タグに便利なgemがたくさんありますので今度gemも使ってみたいと思います。

https://guides.rubyonrails.org/active_record_querying.html

Discussion