[Rails] タグ機能 

ポートフォリオにタグ機能を実装した時のメモです。

# 完成イメージ
投稿画面でタグが投稿でき投稿したタグで絞り込み検索ができる機能です。
![スクリーンショット 2021-10-11 8.06.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974719/9071dbbc-9383-a360-6c57-573e91b47110.png)

# 実装
今回実装する部分の関係性は以下のような関係性です。
![スクリーンショット 2021-10-11 6.56.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974719/41508201-71f5-9255-c0a8-d32d49ecfe81.png)
イベントモデル（CRUD機能）は既に実装済みとします。

### モデル作成
モデルを作っていきます。

```terminal:ターミナル 
$ rails g model tagmap item:references tag:references 
$ rails g model tag tag_name:string
```
タグ投稿するので`tag`モデルには`tag_name`が必要です。

```terminal:ターミナル 
rails db:migrate
```

### リレーション
続いてリレーション

```ruby:app/models/event.rb 
has_many :tagmaps, dependent: :destroy
has_many :tags, through: :tagmaps
```

```ruby:app/models/tag.rb
has_many :tagmaps, dependent: :destroy
has_many :events, through: :tagmaps
```
thoroughを使うことで、tagmaps経由でEventモデルにアクセスできるようになってます。

```ruby:app/models/tagmap.rb
belongs_to :event
belongs_to :tag
```

### タグ作成

タグを作成できるようにしていきます。

```ruby:app/controllers/events_controller.rb
 def create
    @event = current_user.events.build(event_params)

  #--------------------追加した部分----------------------
    if params[:event][:tag_name].present?
      tag_list = params[:event][:tag_name].split(nil)
      @event.save_event_tag(tag_list)
    end
  #----------------------------------------------------

    if @event.save
      redirect_to event_path(@event), notice: '作成しました'
    else
      flash.now[:error] = '作成に失敗しました'
      render :new
    end
  end
```
`split`の引数にnilを指定することで以下の例のように文字列の先頭と末尾の空白文字を除いた上で「空白文字に一致する部分」で分割します。

```terminal:例
irb(main):001:0> " あいう えお か ".split(nil)
=> ["あいう", "えお", "か"]
```
今回はこれを利用し`save_event_tag`というメソッドを作ってcreateできるようにします。

```ruby:app/models/event.rb
def save_event_tag(tags)
  # 既にタグあるなら全取得
  current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?

  # 共通要素取り出し
  old_tags = current_tags - tags
  new_tags = tags - current_tags

  # 古いタグ削除
  old_tags.each do |old_name|
    self.tags.delete Tag.find_by(tag_name:old_name)
  end

  # 新しいタグ作成
  new_tags.each do |new_name|
    post_tag = Tag.find_or_create_by(tag_name:new_name)
    self.tags << post_tag
  end
end
```
タグ投稿の際に同じタグを何度も投稿しないように投稿したタグが既に存在する場合`pluck`メソッドを使って一度タグを全て取得し、タグの共通要素だけを取り出し、古いタグを削除し新しくタグを作成しています。

あとはタグ投稿できるフォーム部分を作れば投稿できます。

```haml:app/views/events/_form.html.haml
%div 
  = f.label :name, 'タグ'
%div
  = f.text_field :tag_name, class: 'form-control'
```

### タグ更新
続いて投稿されたタグを更新できるようにしていきます。

```ruby:app/controllers/events_controller.rb
def edit
  @event = current_user.events.find(params[:id])
#---------------------追加部分-----------------------
  @tag_list = @event.tags.pluck(:tag_name).join(",")
#---------------------------------------------------
end

def update
  @event = current_user.events.find(params[:id])

#---------------------追加部分-----------------------
  if params[:event][:tag_name].present?
    tag_list = params[:event][:tag_name].split(nil)
    @event.save_event_tag(tag_list)
  end
#---------------------------------------------------

  if @event.update(event_params)
    @event.save_event_tag(tag_list)
    redirect_to event_path(@event), notice: '更新しました'
  else
    flash.now[:error] = '更新に失敗しました'
    render :edit
  end
end
```
edit actionでは投稿したタグを`join`で区切って表示しています。
update actionではcreateと同じ処理を書いています。

```haml:app/views/events/_form.html.haml
%div 
  = f.label :name, 'タグ'
%div
  = f.text_field :tag_name, value: @tag_list class: 'form-control'
```

### タグ検索

最後に検索部分です。
検索部分は既にransackを導入済みであることを想定しています。

```ruby:app/controllers/events_controller.rb
def search
  @q = Event.ransack(params[:q])
  @events = @q.result(distinct: true)

 if params[:tag_id].present?
   @tag = Tag.find(params[:tag_id])
   @events = @tag.events.order(created_at: :desc).all
 end

 @tag_lists = Tag.all
end
```
`tag_id`で検索をかけています。

あとはタグ一覧として投稿したタグをHTML上で反映できるようにすれば完成です。

```haml:
.tag 
  %p.tag-title タグ一覧
  - @tag_lists.each do |list|
    = link_to list.tag_name, events_search_path(tag_id: list.id)
```
# 参考

https://qiita.com/Kiyo_Karl2/items/8f3b8ab074bc20e091b6