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

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


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

### 完成イメージ（動画）
![タグ機能.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820462/c00d3c6a-34ac-86ca-85e3-7a6c27ac6ce3.gif)
※記事の最下部にも完成後の静止画像を掲載しているのでご確認ください。
デザインが壊滅的なのはお許しください。。。

# 開発環境
Ruby on Rails 6

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

# 目次
[1.Modelの設計とアソシエーション](#1-Modelの設計とアソシエーション)
[2.Modelを生成](#2-Modelを生成)
[3.マイグレーションファイルの確認](#3-マイグレーションファイルの確認)
[4.Modelファイルの確認](#4-Modelファイルの確認)
[5.Controllerに追記](#5-Controllerに追記)
[6.インスタンスメソッドを定義](#6-Postモデルファイルに、save_tagインスタンスメソッドを定義)
[7.Viewの作成](#7-Viewの作成)
[8.ルーティングの設定](#8-ルーティングの設定)
[9.タグで投稿を絞り込み表示する機能の実装](#9-タグで投稿を絞り込み表示する機能の実装)
[10.タグの編集と削除機能](#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マイグレーションファイル
```ruby: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マイグレーションファイル
```ruby: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マイグレーションファイル
```ruby: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
```ruby: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
```ruby: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
```ruby: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
```ruby: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アクションを定義し直していきます。
```ruby: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
```
### コードの説明
>```ruby
>@post = current_user.posts.new(post_params)
>```
>`current_user.posts`
>このように記述することで、ログイン中のユーザーのidがpostsテーブルのuser_idに保存されるようになりますが、UserとPostの関連付けを行っていない場合はエラーになります。引数にはStrong Parametersを使っています。

>```ruby
>tag_list = params[:post][:tag_name].split(nil)
>```
>`params[:post][:tag_name]`
>formから、@postオブジェクトを参照してタグの名前も一緒に送信するのでこの形で取得します。
例として、["タグ" "検索" "機能"]みたいな感じで送られてきます。
>
>`.split(nil)`
>送信されてきた値を「**スペース**」で区切って配列化します。上記の例でいうと["タグ" "検索" "機能"]みたいな感じです。配列化するのは、あとでこの値をデータベースに保存する際に一つずつ繰り返し処理で取り出す必要があるためです。
もし、「**,**」などで区切りたい場合は **.split(,)** で実装することができます。

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

### 5-2. new,index,showアクション
次に、タグを投稿するためにnewnew,index,showアクションに追記していきます。**3点**追記しています。
```ruby: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インスタンスメソッドの中身を定義していきます。
```ruby: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
```
### コードの説明
>```ruby
>current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?
>```
>`現在存在するタグの取得`
>先ほどcreateアクションにて保存した@postに紐付いているタグが存在する場合、「タグの名前を配列として」全て取得します。

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

>```ruby
>new_tags = sent_tags - current_tags
>```
>`新しいタグの取得`
送信されてきたタグから、現在存在するタグを除いたタグを**new_tags**とします。

>```ruby
>old_tags.each do |old|
>    self.tags.delete Tag.find_by(tag_name: old)
>end
>```
>`新しいタグの保存`
>古いタグを削除します。

>```ruby
>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 ルーティングの設定
```ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users

  root 'posts#index'

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

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

### 9-1. ルーティングの追加
先ほど確認したルーティングに追加で記述していきます。
```ruby: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アクションを追加していきます。
```ruby: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に編集と削除の記述をしていきます。ここはすごくややこしいです。
```ruby: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
```
一旦ここまでで、編集と削除は可能になりました。
ハッシュタグ風の追加できるタグ機能の実装はここまでになります。お疲れ様でした。
[ハッシュタグ風タグ機能②](https://qiita.com/kizakey18/items/8db736c63b50a2cb2581)にて、デザインの実装をしているのでよかったら参考にしてください。

# 完成イメージ（一覧ページ）
![スクリーンショット 2022-07-24 3.06.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820462/1b3f7c17-26c3-928d-5321-a191550105b3.png)
↑編集と削除機能を追加する前のものなので、イメージ画像には編集削除機能は載ってません。
****
尚、今回の記事では、viewページのデザイン性は皆無ですm(_ _)m


デザイン関連に関しては続編
[ハッシュタグ風タグ機能②](https://qiita.com/kizakey18/items/8db736c63b50a2cb2581)
にて以下の機能とデザインについて取り扱ってるので余力がある方は取り組んでみてください！
1. タグ一覧ページの追加
2. タグのデザイン変更
3. タグ検索の導入
4. タグの数ランキング




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