【Ruby on Rails】タグ付機能/タグ絞り込み機能の実装

# はじめに
Railsで`gem acts-as-taggable-on`を用いて、タグ機能を実装しました。
仕様としては、よくあるパターンでインスタンス作成画面でユーザーが任意のタグを入力して保存するというものです。
↓
[実装イメージ](https://gyazo.com/f872ae844e4e4d70ecb15d71e8b658b0)

イメージのように
①new画面でインスタンスを作成

②confirm画面で入力内容を確認

③createアクション実行で一覧画面にリダイレクト

④タグをクリックすると、そのタグに紐づくインスタンスが一覧表示される

それでは実装に入ります！

# 前提（開発環境）

*  **macOS Catalina**
*  **Ruby 2.6.5**
*  **Ruby on Rails 6.0.0**
*  **Visual Studio Code**
*  **データベース: Mysql**
*  **テンプレートエンジン: haml**

# 目次
[1.事前準備](#1-事前準備)
[2.ModelとController](#2-ModelとController)
[3.View](#3-View)

## 1. 事前準備
今回はacts-as-taggable-onというGemを使うのですが、インストールの途中でエラーが発生してしまうので、そのエラーがでないように実装していきましょう。順番に記載します。

```php:Gemfile
gem 'acts-as-taggable-on', '~> 6.0' #追加
```

```php:ターミナル
% bundle install
```

```php:ターミナル
% rails acts_as_taggable_on_engine:install:migrations
```

`rails acts_as_taggable_on_engine:install:migrations`を実行すると必要なマイグレーションファイルが作成されます。通常はここで`rails db:migrate`を実行してデータベースに反映させるのですが、Mysqlを使用している場合、ここで必要な処理を行わないとエラーが起きてしまいます。

もしも、migrateさせてしまった場合は`rails db:rollback`でファイルを戻しましょう。

[acts-as-taggable-on公式リファレンス](https://github.com/mbleigh/acts-as-taggable-on)の通りターミナルでコマンドを実行します。
![スクリーンショット 2020-12-23 10.49.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/772095/9e7c478a-e241-bf02-9616-166b13daafca.png)

```php:ターミナル
% rails acts_as_taggable_on_engine:tag_names:collate_bin
```

次に先程生成されたマイグレーションファイルを編集します。（ファイル名に気をつけてください）

```php:db/migrate/・・・・_add_missing_unique_indices.acts_as_taggable_on_engine.rb
# This migration comes from acts_as_taggable_on_engine (originally 2)
if ActiveRecord.gem_version >= Gem::Version.new('5.0')
  class AddMissingUniqueIndices < ActiveRecord::Migration[4.2]; end
else
  class AddMissingUniqueIndices < ActiveRecord::Migration; end
end
AddMissingUniqueIndices.class_eval do
  def self.up
    add_index ActsAsTaggableOn.tags_table, :name, unique: true

    # remove_index ActsAsTaggableOn.taggings_table, :tag_id if index_exists? (ActsAsTaggableOn.taggings_table, :tag_id)　#コメントアウトする
    if index_exists?(ActsAsTaggableOn.taggings_table, :tag_id) #追加
      remove_foreign_key :taggings, :tags                      #追加
      remove_index ActsAsTaggableOn.taggings_table, :tag_id    #追加
    end
    remove_index ActsAsTaggableOn.taggings_table, name: 'taggings_taggable_context_idx'
    add_index ActsAsTaggableOn.taggings_table,
              [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type],
              unique: true, name: 'taggings_idx'
  end

  def self.down
    remove_index ActsAsTaggableOn.tags_table, :name

    remove_index ActsAsTaggableOn.taggings_table, name: 'taggings_idx'

    add_index ActsAsTaggableOn.taggings_table, :tag_id unless index_exists?(ActsAsTaggableOn.taggings_table, :tag_id)
    add_index ActsAsTaggableOn.taggings_table, [:taggable_id, :taggable_type, :context], name: 'taggings_taggable_context_idx'
  end
end
```
①11行目をコメントアウト

```php:db/migrate/・・・・_add_missing_unique_indices.acts_as_taggable_on_engine.rb
# remove_index ActsAsTaggableOn.taggings_table, :tag_id if index_exists?(ActsAsTaggableOn.taggings_table, :tag_id)
```
②12行目から以下の記述を追加

```php:db/migrate/・・・・_add_missing_unique_indices.acts_as_taggable_on_engine.rb
if index_exists?(ActsAsTaggableOn.taggings_table, :tag_id) #追加
  remove_foreign_key :taggings, :tags                      #追加
  remove_index ActsAsTaggableOn.taggings_table, :tag_id    #追加
end
```
これで処理はバッチリです！

```php:ターミナル
% rails db:migrate
```

次に一覧画面に表示しておくタグをseed.rbに入れておきます。

```php:db/seed.rb
# タグ付紐付け(%w()の中に記述)
array = %w(ダンス 歌 撮影モデル サロン・カットモデル 演技 マジック アイドリング エキストラ 楽器演奏 リモート可能 東京 神奈川 埼玉 千葉 群馬 茨城 兵庫 北海道 大阪 京都 愛知 福岡 5000円~ 10000円~ 15000円~ 20000円~ 25000円~ 30000円~ 積極募集 Youtube広告モデル)
array.each{ |tag|
  tag_list = ActsAsTaggableOn::Tag.new
  tag_list.name = tag
  tag_list.save
}
```

```php:ターミナル
% rails db:seed
```
`rails db:seed`がうまくいくとMysqlにデータが保存されると思います。
![スクリーンショット 2020-12-23 11.17.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/772095/2ae3722e-247c-fa8e-38a2-1cccf77918e4.png)

ここまでで事前準備は整いました。

## 2. ModelとController
タグを付与したいモデルに以下の記述を追加します。

```php:app/models/event.rb
class Event < ApplicationRecord
    acts_as_taggable  #追加
end
```

```php:app/controllers/events_controller.rb
class EventsController < ApplicationController

  def index
　　#一覧画面にタグを表示
    @tags1 = ActsAsTaggableOn::Tag.where("id < ?", 10)
    @tags2 = ActsAsTaggableOn::Tag.where(id: 11..22)
    @tags3 = ActsAsTaggableOn::Tag.where(id: 23...29)
    @tags4 = ActsAsTaggableOn::Tag.where(id: 29...31)
　　# タグ検索時にそのタグずけしているものを表示
    if params[:tag_name]
      @events = Event.tagged_with("#{params[:tag_name]}").includes(:recruiter)
    else
      @events = Event.all.includes(:recruiter)
    end
  end

  def new
    @event = Event.new
  end
  
  def confirm
    @event = Event.new(event_params)
    if @event.invalid?
      render :new
    end
  end

  def create
    @event = Event.new(event_params)
    @event.recruiter_id = current_user.id
    render :new and return if params[:back] || !@event.save
    if @event.save
      redirect_to root_path
    else
      render :new
    end
  end
  
  def show
    @event = Event.find_by(id: params[:id])
  end
  
  private

  def event_params
    params.require(:event).permit(:event_name, :datetime, :prefecture, :place, :detail, :tag_list)
  #ストロングパラメーターに :tag_listを追加
  end

  def set_user
    @user = current_user
  end

end
```
解説は後にまとめて行います。先にViewパートに移ります。

## 3.View

### インスタンスにタグ付けを行う

```php:views/events.new.html.haml
 = form_with(model: @event, url: confirm_events_path(@event), local: true, id: "new-event") do |f|
     .
     .

   .create-item-2
     = f.label :tag_list, "タグ付け"
     = f.text_field :tag_list, value: @event.tag_list.join(','), placeholder: "ダンス,東京,・・・"
     .
     .

   .create-btn-field
     = f.submit "確認画面へ"
```

```php:views/events.new.html.haml
.create-item-2
     = f.label :tag_list, "タグ付け"
     = f.text_field :tag_list, value: @event.tag_list.join(','), placeholder: "ダンス,東京,・・・"
```
上記の部分を`form_with`にネストさせることで**「`@event`に対してタグを付与させますよ」**という意味になります。

### `url: confirm_events_path(@event)`
となっていますが、確認画面を挟まない場合は`= form_with(model: @event, local: true, id: "new-event") do |f|`で大丈夫です。
コントローラーの部分もconfirmアクションは必要ありません。
確認画面の実装は[こちらを御覧ください](https://qiita.com/anago1030/items/0016bbf99e7fe3b926e1)

### `@event.tag_list.join(',')`
カンマで区切ることによって、入力されたタグが配列の形でデータが保存されます。

### 一覧画面にタグを表示させる

```php:views/events.html.haml
  %p.tag-word
    ジャンルで検索
   - @tags1.each do |genre|
     = link_to "#{genre.name}(#{genre.taggings_count})", events_path(tag_name: genre.name), class: "tag-link"
```

コントローラーの部分で`@tag1`はこのように定義されています

```php:app/controllers/events_controller.rb
def index
　　#一覧画面にタグを表示
    @tags1 = ActsAsTaggableOn::Tag.where("id < ?", 10)
```
意味としては**「seed.rbで事前に作成したタグの中からidが10番未満のタグを`@tags1`に代入」**となります。
viewでは`@tags1`を`each`メソッドで一覧表示しています。
残りの`@tags2``@tags3``@tags4`も同じ手順で一覧画面に表示しています。

### (#{genre.taggings_count})
`.taggings_count`メソッドはタグに紐付いているインスタンスの数をviewに表示してくれます。

### events_path(tag_name: genre.name)
タグをクリックすると、そのタグが紐付いているインスタンスを一覧表示します。

```php:app/controllers/events_controller.rb
def index
  .
  .
  if params[:tag_name]
    @events = Event.tagged_with("#{params[:tag_name]}").includes(:recruiter)
  else
    @events = Event.all.includes(:recruiter)
  end
end
```

`Event.tagged_with("#{params[:tag_name]}")`とすることでタグが紐付いているインスタンスの情報を全て取得してくれます。

実装は以上です！

# おわりに
最後まで読んでいただきありがとうございました！
お疲れ様でした。。

# 参考文献
[Rails | acts-as-taggable-on を使ったタグ機能の実装 | 備忘録](https://qiita.com/ddd555/items/e1caa8b73d118822a0a2#4-%E3%82%BF%E3%82%B0%E3%81%AE%E7%B5%9E%E3%82%8A%E8%BE%BC%E3%81%BF%E6%A9%9F%E8%83%BD%E3%81%AE%E8%BF%BD%E5%8A%A0)
[Railsにタグ機能をつける。acts-as-taggable-on使用](https://qiita.com/nakamura999/items/56c6426fc5dd640617ce)