2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Rails】ハッシュタグ風タグ機能①

Last updated at Posted at 2022-08-04

こんにちは!
この記事では、railsで 「ハッシュタグ風タグ機能」 を実装していきます!
これは、gemなしでタグを自由に追加することができ、タグの表示はもちろん、タグごとに投稿一覧を見ることができる機能です。

ゴール

gemなしで、railsで投稿に対してタグ付けできるようにし、つけられたタグごとに投稿一覧を表示することができる機能の実装。

完成イメージ(動画)

タグ機能.gif
※記事の最下部にも完成後の静止画像を掲載しているのでご確認ください。
デザインが壊滅的なのはお許しください。。。

開発環境

Ruby on Rails 6

前提

  • deviseのインストール
  • 投稿周りの機能は実装していること。(今回はPostにしています)
  • post_idカラムが追加されていること。

目次

1.Modelの設計とアソシエーション
2.Modelを生成
3.マイグレーションファイルの確認
4.Modelファイルの確認
5.Controllerに追記
6.インスタンスメソッドを定義
7.Viewの作成
8.ルーティングの設定
9.タグで投稿を絞り込み表示する機能の実装
10.タグの編集と削除機能

実装

1 Modelの設計とアソシエーション

Model設計とアソシエーションの詳しい説明は後日更新しますm(_ _)m

2 Modelを生成

まずは、上記で設計した通りにモデルを作成していきます。

$ rails g devise User                                   //既に作成している場合は省略
$ rails g model Post content:string body:text           //既に作成している場合は省略

$ rails g model Tag tag_name:string                     //Tagモデル作成
$ rails g model TagMap post:references tag:references  //TagMapモデル作成
$ rails db:migrate

モデルを作る際に、後ろに指定したいモデル名:referencesを追記することで、指定したモデルとのアソシエーション関係を作成の時点で自動で作ることができます。

3 マイグレーションファイルの確認

3-1. postsマイグレーションファイル

db/migrate/_create_posts.rb
class CreatePosts < ActiveRecord::Migration[6.1]
  def change
    create_table :posts do |t|
      t.string :content
      t.text :body
      t.integer :user_id

      t.timestamps
    end
  end
end

3-2. tagsマイグレーションファイル

db/migrate/_create_tags.rb
class CreatePostTags < ActiveRecord::Migration[6.1]
  def change
    create_table :post_tags do |t|
      t.string :tag_name

      t.timestamps
    end
  end
end

3-3. tag_mapsマイグレーションファイル

db/migrate/_create_tag_maps.rb
class CreatePostTagMaps < ActiveRecord::Migration[6.1]
  def change
    create_table :tag_maps do |t|
      t.references :post, foreign_key: true
      t.references :tag, foreign_key: true

      t.timestamps
    end
  end
end

4 Modelファイルの確認

4-1. User Model

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :posts, dependent: :destroy
end

dependent: :destroy
親クラスに付けることで、親クラスが削除されたときに子クラスも一緒に削除することができるオプション。

4-2. Post Model

app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user

  has_many :tag_maps, dependent: :destroy
  has_many :tags, through: :tag_maps
end

through
tag_mapsテーブルを通してtagsテーブルとの関連付けを行っています。こうすることで、Post.tagsとすればPostに紐付けられたTagの取得が可能になるオプション。これは投稿詳細画面などでその投稿に付けられているタグを取得して表示させるなどの際に使えます。

・throughオプションを使う場合、先にその中間テーブルとの関連付けを行う必要がある。
・中間テーブルにdependent: :destroyオプションを付けることで、Postが削除されると同時にPostとTagの関係が削除される。

4-3. Tag Model

app/models/tag.rb
class Tag < ApplicationRecord
  has_many :tag_maps, dependent: :destroy, foreign_key: 'tag_id'
  has_many :posts, through: :tag_maps
end

tag_mapsテーブルとの関連付けを行ってから、tag_mapsを通してpostsテーブルと関連づけています。Tag.postsとすれば、タグに紐付けられたPostを取得できます。

4-4. Tag Model

app/models/tag_map.rb
class TagMap < ApplicationRecord
  belongs_to :post
  belongs_to :tag

  validates :post_id, presence: true
  validates :tag_id, presence: true
end

belongs_to
複数のPost、複数のTagに所有されるのでbelongs_toで関連付けます。これは、先ほどのモデル名:referencesにて自動的に記述されています。

validates
バリデーション(制限)をつけます。オブジェクトがDBに保存される前に、そのデータが正しいかどうかを検証する仕組みをバリデーションといい、今回はではないことを制限しています。

5 Controllerに追記

次に、postsコントローラーに追記していきます。

5-1. createアクション

まずは、createアクションを定義し直していきます。

app/controllers/posts_controller.rb
class PostsController < ApplicationController

# 割愛

#  変更前
 def create
    @post = Post.new(post_params) 
    @post.user_id = current_user.id 
    @post.save
     redirect_to posts_path
 end
# この部分を下のように変更してください

# 変更後
 def create
    @post = current_user.posts.new(post_params)           
    tag_list = params[:post][:tag_name].split(nil)  
    if @post.save
      @post.save_tag(tag_list)                                                           
      redirect_to posts_path      
    else
      redirect_to new_post_path         
    end
 end
# このように変更してください

 private
   def post_params
     params.require(:post).permit(:body, :content)
   end
end

コードの説明

@post = current_user.posts.new(post_params)

current_user.posts
このように記述することで、ログイン中のユーザーのidがpostsテーブルのuser_idに保存されるようになりますが、UserとPostの関連付けを行っていない場合はエラーになります。引数にはStrong Parametersを使っています。

tag_list = params[:post][:tag_name].split(nil)

params[:post][:tag_name]
formから、@postオブジェクトを参照してタグの名前も一緒に送信するのでこの形で取得します。
例として、["タグ" "検索" "機能"]みたいな感じで送られてきます。

.split(nil)
送信されてきた値を「スペース」で区切って配列化します。上記の例でいうと["タグ" "検索" "機能"]みたいな感じです。配列化するのは、あとでこの値をデータベースに保存する際に一つずつ繰り返し処理で取り出す必要があるためです。
もし、「,」などで区切りたい場合は .split(,) で実装することができます。

@post.save_tag(tag_list)

先ほど取得したタグの配列をデータベースに保存する処理です。その処理を行うsave_tagの定義は後述します。

5-2. new,index,showアクション

次に、タグを投稿するためにnewnew,index,showアクションに追記していきます。3点追記しています。

app/controllers/posts_controller.rb
class PostsController < ApplicationController

# 割愛

 def new
    @post = Post.new                 # 新規投稿用の空のインスタンス
   # 追記箇所1
    @post = current_user.posts.new   # ビューのform_withのmodelに使う。
   # ここまで
 end

 def index
    @posts = Post.all
   # 追記箇所2
    @tag_list = Tag.all              #ビューでタグ一覧を表示するために全取得。
   # ここまで      
 end

 def show
    @post = Post.find(params[:id])
   # 追記箇所3
    @post_tags = @post.tags         #そのクリックした投稿に紐付けられているタグの取得。
   # ここまで
 end

# 割愛

end

6 Postモデルファイルに、save_tagインスタンスメソッドを定義

先ほど、5-1でcreateアクションで記述したsave_tagインスタンスメソッドの中身を定義していきます。

app/models/post.rb
class Post < ApplicationRecord

  # 割愛

   def save_tag(sent_tags)
     current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?
     old_tags = current_tags - sent_tags
     new_tags = sent_tags - current_tags

     old_tags.each do |old|
       self.tags.delete Tag.find_by(tag_name: old)
     end

     new_tags.each do |new|
       new_post_tag = Tag.find_or_create_by(tag_name: new)
       self.tags << new_post_tag
     end
   end
end

コードの説明

current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?

現在存在するタグの取得
先ほどcreateアクションにて保存した@postに紐付いているタグが存在する場合、「タグの名前を配列として」全て取得します。

old_tags = current_tags - sent_tags

古いタグの取得
現在取得した@postに存在するタグから、送信されてきたタグを除いたタグをold_tagsとします。

new_tags = sent_tags - current_tags

新しいタグの取得
送信されてきたタグから、現在存在するタグを除いたタグをnew_tagsとします。

old_tags.each do |old|
   self.tags.delete Tag.find_by(tag_name: old)
end

新しいタグの保存
古いタグを削除します。

new_tags.each do |new|
   new_post_tag = Tag.find_or_create_by(tag_name: new)
   self.post_tags << new_post_tag
end

新しいタグの保存
新しいタグをデータベースに保存する。

7 Viewの作成

7-1. newページ

app/views/posts/new.html.erb
<%= form_with(model: @post, url: posts_path, local: true) do |f| %>
  <p>
    <%= f.label :body,"投稿" %>
    <%= f.text_field :body %>
    <br>
    <%= f.label :content,"内容" %>
    <%= f.text_field :content %>
  </p>
  <br>
  <p>スペースを入力することで複数のタグを付けることができます。</p>
  <%= f.text_field :tag_name, placeholder: "Ruby Python Java" %>
  <%= f.submit %>
<% end %>

placeholder
placeholderを使えば、入力欄に初期表示する内容を指定することができます。

7-2. index,showページ

実際にタグを表示させるビューを作成していきます。
変数(list,post,tag)に気をつけて記述してください。

app/views/posts/index.html.erb
<h3>タグリスト</h3>
<% @tag_list.each do |list| %>
  #<%= link_to list.tag_name, tag_posts_path(tag_id: list.id) %>
  <%= "(#{list.posts.count})" %>
<% end %>

<% @posts.each do |post| %> 
#割愛
  <% @posts.each do |post| %> 
      #<%= link_to tag.tag_name, tag_posts_path(tag_id: tag.id) %>
  <% end %>
<% end %>
app/views/posts/show.html.erb
<% @post_tags.each do |tag| %>
  <% if tag.posts.count > 0 %>
    #<%= link_to tag.tag_name, tag_posts_path(tag_id: tag.id) %>
    <%= "(#{tag.posts.count})" %>
  <% end %>
<% end %>

これでタグの投稿から、表示まで一通りの機能が完成しました。

8 ルーティングの設定

config/routes.rb
Rails.application.routes.draw do
  devise_for :users

  root 'posts#index'

  resources :posts
end

基本的に上記3つが記述されてれば問題ありません。

9 タグで投稿を絞り込み表示する機能の実装

次に、投稿したタグ一覧から各タグのカテゴリーごとにページ遷移(絞り込み表示)できるように実装していきます。

9-1. ルーティングの追加

先ほど確認したルーティングに追加で記述していきます。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root 'posts#index'
  resources :posts

  # 追加
  resources :tags do
    get 'posts', to: 'posts#search'
  end
  # ここまで
end

ネストすることで、先ほど記述したような tag_posts_path(tag_id: tag.id) という、特定のタグに紐付けられた投稿ページへ遷移させるためのパスが使えるようになります。

9-2. searchアクションの追加

postsコントローラーにsearchアクションを追加していきます。

app/controllers/posts_controller.rb
def search
    @tag_list = Tag.all               # こっちの投稿一覧表示ページでも全てのタグを表示するために、タグを全取得
    @tag = Tag.find(params[:tag_id])  # クリックしたタグを取得
    @posts = @tag.posts.all           # クリックしたタグに紐付けられた投稿を全て表示
end

9-3. 絞り込んだ後のViewページの作成

app/views/postsディレクトリ内に、search.html.erbファイルを手動で作成します。

app/views/posts/search.html.erb
<h3>タグリスト</h3>
<% @tag_list.each do |list| %>
  <%= list.tag_name %>
  <%= "(#{list.posts.count})" %>
<% end %>
<br>
<%= "#{@tag.tag_name}" %>
<br>
<% @posts.each do |post| %>
  <%= link_to post.content, post %>
<% end %>

10 タグの編集と削除機能

最後に、タグの編集と削除ができるような機能を実装していきます。

10-1. viewページの追加

編集できるようにviewページを更新していきます。

app/views/posts/edit.html.erb
<%= form_for @post do |f| %>
    <div class="field">
        <%= f.label :body %>
        <%= f.text_field :body, :size => 140 %> 
    </div>

  # ===========追記================
    <div class="field">
        <%= f.label"タグ (スペースで区切ると複数タグ登録できます)" %>
        <%= f.text_field :tag_name, value: @tag_list,class:"form-control"%>
    </div>
  # ==============================

    <%= f.submit "編集する" %>
<% end %>

value: @tag_listにしてあげることで、フォームに元の情報を入れて出力してます。

10-2. controllerの編集

次に、controllerに編集と削除の記述をしていきます。ここはすごくややこしいです。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
   #省略

  def edit
    @post = Post.find(params[:id])
    # tagの編集
    @tag_list=@post.tags.pluck(:tag_name).join(nil)
  end

  def update
    post = Post.find(params[:id])
    # tagの編集&削除
    tag_list = params[:post][:tag_name].split(nil)
    # もしpostの情報が更新されたら
    if post.update(post_params)
      # このpost_idに紐づいていたタグを@oldに入れる
      old_relations = TagMap.where(post_id: post.id)
      # それらを取り出し、消す。
      old_relations.each do |relation|
        relation.delete
      end

      post.save_tag(tag_list)
      redirect_to post_path(post.id), notice:'投稿完了しました:)'
    else
      redirect_to :action => "edit"
    end

  end

end

一旦ここまでで、編集と削除は可能になりました。
ハッシュタグ風の追加できるタグ機能の実装はここまでになります。お疲れ様でした。
ハッシュタグ風タグ機能②にて、デザインの実装をしているのでよかったら参考にしてください。

完成イメージ(一覧ページ)

スクリーンショット 2022-07-24 3.06.57.png
↑編集と削除機能を追加する前のものなので、イメージ画像には編集削除機能は載ってません。


尚、今回の記事では、viewページのデザイン性は皆無ですm(_ _)m

デザイン関連に関しては続編
ハッシュタグ風タグ機能②
にて以下の機能とデザインについて取り扱ってるので余力がある方は取り組んでみてください!

  1. タグ一覧ページの追加
  2. タグのデザイン変更
  3. タグ検索の導入
  4. タグの数ランキング

GeekSalonという大学生限定のプログラミングコミュニティではメンターが精力的に記事を書いています!応援よろしくお願いします!

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?