こんにちは!
この記事では、railsで 「ハッシュタグ風タグ機能」 を実装していきます!
これは、gemなしでタグを自由に追加することができ、タグの表示はもちろん、タグごとに投稿一覧を見ることができる機能です。
ゴール
gemなしで、railsで投稿に対してタグ付けできるようにし、つけられたタグごとに投稿一覧を表示することができる機能の実装。
完成イメージ(動画)
※記事の最下部にも完成後の静止画像を掲載しているのでご確認ください。
デザインが壊滅的なのはお許しください。。。
開発環境
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マイグレーションファイル
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マイグレーションファイル
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マイグレーションファイル
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
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
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
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
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アクションを定義し直していきます。
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点追記しています。
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インスタンスメソッドの中身を定義していきます。
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ページ
<%= 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)に気をつけて記述してください。
<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 %>
<% @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 ルーティングの設定
Rails.application.routes.draw do
devise_for :users
root 'posts#index'
resources :posts
end
基本的に上記3つが記述されてれば問題ありません。
9 タグで投稿を絞り込み表示する機能の実装
次に、投稿したタグ一覧から各タグのカテゴリーごとにページ遷移(絞り込み表示)できるように実装していきます。
9-1. ルーティングの追加
先ほど確認したルーティングに追加で記述していきます。
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アクションを追加していきます。
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ファイルを手動で作成します。
<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ページを更新していきます。
<%= 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に編集と削除の記述をしていきます。ここはすごくややこしいです。
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
一旦ここまでで、編集と削除は可能になりました。
ハッシュタグ風の追加できるタグ機能の実装はここまでになります。お疲れ様でした。
ハッシュタグ風タグ機能②にて、デザインの実装をしているのでよかったら参考にしてください。
完成イメージ(一覧ページ)
↑編集と削除機能を追加する前のものなので、イメージ画像には編集削除機能は載ってません。
尚、今回の記事では、viewページのデザイン性は皆無ですm(_ _)m
デザイン関連に関しては続編
ハッシュタグ風タグ機能②
にて以下の機能とデザインについて取り扱ってるので余力がある方は取り組んでみてください!
- タグ一覧ページの追加
- タグのデザイン変更
- タグ検索の導入
- タグの数ランキング
GeekSalonという大学生限定のプログラミングコミュニティではメンターが精力的に記事を書いています!応援よろしくお願いします!