【Rails】タグ管理機能(acts-as-taggable-on使用)

- タグ付け機能を実装時の記録。エラーで戸惑ったので、備忘録として。
- acts-as-taggable-on(gem)、UI に Tag-it( jQueryのプラグイン )を使用。

![Alt text](https://gyazo.com/6f00948da1272468dab65e5ed6caa386/raw)


# 環境
- macOS Catalina
- ruby 2.5.1
- rails 5.0.7.2
- acts-as-taggable-on 6.5.0  ( ※ ActiveRecord のモデルにタグ付けしてくれるGem)
- DB mySQL

# 【 タグ付け機能 ： 登録/削除/表示 】
## 1. gem導入
 - [acts-as-taggable-onのGitHub](https://github.com/mbleigh/acts-as-taggable-on) 通りやると、マイグレーションエラー発生。

```rb:Gemfile
gem 'acts-as-taggable-on', '~> 6.0'
```
```rb:ターミナル
% bundle install
         :
# マイグレーションファイルをインストールしてね！というメッセージが表示されるので、従う ↓
  % rake acts_as_taggable_on_engine:install:migrations
    # 6個のマイグレーションファイルが作成される
    Copied migration 20200905143628_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb from acts_as_taggable_on_engine
    Copied migration 20200905143629_add_missing_unique_indices.acts_as_taggable_on_engine.rb from acts_as_taggable_on_engine
    Copied migration 20200905143630_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb from acts_as_taggable_on_engine
    Copied migration 20200905143631_add_missing_taggable_index.acts_as_taggable_on_engine.rb from acts_as_taggable_on_engine
    Copied migration 20200905143632_change_collation_for_tag_names.acts_as_taggable_on_engine.rb from acts_as_taggable_on_engine
    Copied migration 20200905143633_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb from acts_as_taggable_on_engine

# mySQLの場合は、マイグレーション実行前に、↓ を実行が必要（初期設定のため）
% rake acts_as_taggable_on_engine:tag_names:collate_bin
% rails db:migrate   # それでも、エラー発生(gemのバグ？)
```
- マイグレーションファイルを修正。  
※ 外部キーを削除せずに、インデックスを削除しよーとしてるのが原因みたいなので、インデックス削除前に外部キー部分をコメントアウトで対応。

```rb:xxxxxxxxx_add_missing_unique_indices.acts_as_taggable_on_engine.rb
         :
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)
    # 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
```
```rb:xxxxxxxxxx_add_missing_taggable_index.acts_as_taggable_on_engine.rb
         :
AddMissingTaggableIndex.class_eval do
  def self.up
    # add_index ActsAsTaggableOn.taggings_table, [:taggable_id, :taggable_type, :context], name: 'taggings_taggable_context_idx'
  end

  def self.down
    # remove_index ActsAsTaggableOn.taggings_table, name: 'taggings_taggable_context_idx'
  end
end
```
**【 作成されたテーブルとカラム 】**

|tags テーブル|taggings テーブル|
|:--|:--|
|name(タグ名)|tag_id(tagsテーブルのid)|
|taggings_count(タグの登録数)|taggable_type|
||taggable_id|
||tagger_type|
||tagger_id|
||content|
※ tagsテーブルは、unique: true(一意性)がかかってる。

### 例 ： acts-as-taggable-onのメソッド
※ 詳細は、リファレンス参照。

#### 【 取得や検索 】

|メソッドなど|意味|
|:--|:--|
|tags_on(:tags)|全 Article のタグ一覧取得|
|most_used|登録数が多いタグ|
|least_used|登録数が少ないタグ|
|most_used(10)|登録数が多いタグから取得。デフォルトは20|
|least_used(10)|登録数が少ないタグから取得。デフォルトは20|
|tag_counts|全タグのデータ|
|named("タグ名")|完全一致|
|named_any(["タグ名1", "タグ名2",..])|完全一致(and)|
|named_like("タグ名")|部分一致|
|named_like_any(["タグ名1", "タグ名2",..])|部分一致(or)|


```rb:例)タグ検索
class User < ActiveRecord::Base
  acts_as_taggable_on :tags, :skills
  scope :by_join_date, order("created_at DESC")
end
  User.tagged_with(params[:tag])       # タグに紐づくUserのデータを取得
  User.tagged_with("タグ1")[0].id       # 1
  User.tagged_with("タグ1, タグ2")[0].id # 配列ではなく、コンマ区切りの文字列でも可。
  # 含むか？
  User.tagged_with("タグ1").by_join_date
  User.tagged_with("タグ1").by_join_date.paginate(page: params[:page], per_page: 20)
  # 完全一致(AND検索)
  User.tagged_with(["タグ1", "タグ2"], match_all: true)
  # 条件一致(OR検索)
  User.tagged_with(["タグ1", "タグ2"], any: true)
  # 除外(含まないモノを検索)
  User.tagged_with(["タグ1", "タグ2"], exclude: true)
```




#### 【 登録や削除 】

|メソッドなど|意味|
|:--|:--|
|tag_list.add("タグ1", "タグ2", ..)|追加|
|tag_list = 'タグ1, タグ2, ..'|上書き|
|tag_list.remove("タグ1", "タグ2", ..)|削除|
|save|保存 ([id: 1, name: "タグ1", taggings_count: 1],[id: 2, name: "タグ2", taggings_count: 1])|



## 2. モデル（アソシエーション）
- タグ付けしたいモデルに、アソシエーションを追加。

```rb:Postモデル
acts_as_taggable   # acts_as_taggable_on :tags の省略

# 参)複数設定も可能↓
acts_as_taggable_on :skills, :interests  # @post.skill_list とかが使えるようになる
```











## 3. コントローラー
- タグ登録のため、ストロングパラメーターに、`:tag_list`を追加。
- タグ表示のため、アクションも追加。

```rb:postsコントローラー
def index
  @posts = Post.all
  @tags = Post.tag_counts_on(:tags).most_used(20)    # タグ一覧表示
end

def show
  @post = Post.find(params[:id])
  @tags = @post.tag_counts_on(:tags)    # 投稿に紐付くタグの表示
end
      :
private
      :
def post_params
  params.require(:post).permit(:title, :content, :tag_list)
end
```







## 4. ビュー
### 4-1. タグ付け用フォーム
- `,` (デフォルト)で区切ると、複数タグに分割してくれる。

```haml:投稿ページにタグ付け用のフォーム設置(haml)
- form_for @post do |f|
　　　　:
  = f.label :tag_list
  = f.text_field :tag_list, value: @post.tag_list.join(',')

-# 参)タグのチェックボックスで選択させたい時
- @tags.each do |tag|
  = f.check_box :tag_list, { multiple: true }, "#{tag.name}", nil
  = f.label " #{tag.name}(#{tag.taggings_count})"
```



### 4-2. タグの表示
- モデルで追加した`:tags`がPostモデルに紐づいてる。
- tag_listは配列なので、each文で取得。  
![Alt text](https://gyazo.com/aeb5dc2701020b72d61a4872a84bc4ce/raw)

```haml:タグの表示(haml)
- if @tags.present?
  - @tags.each do |tag|  # コントローラーで、登録数の順で20個取得(＠tags)
    = link_to "#{tag.name}(#{tag.taggings_count})", tags_path(tag.name)
- else
 %p 登録されているタグはありません
```













# 【 タグ検索 】
- タグ（リンク）をクリックすると、posts#indexページに、関連する投稿一覧を表示する。

```rb:postsコントローラー
def index
       :
  @tags = Post.tag_counts_on(:tags).order('count DESC')     # 全タグ(Postモデルからtagsカラムを降順で取得)
  if @tag = params[:tag]   # タグ検索用
    @post = Post.tagged_with(params[:tag])   # タグに紐付く投稿
  end
end
```
- `tagged_with("タグ名")` : 絞り込み検索するメソッド。  
クリックされたtag情報を取得し、tagged_with("タグ名")で検索。同じタグを持つ投稿を取得できる。




```haml:リンク付きのタグ（haml）
- @tags.each do |tag|
  = link_to "#{tag.name}(#{tag.taggings_count})", posts_path(tag: tag.name)
```
```haml:タグに紐付く投稿一覧の表示（haml）
- if @post.present?
  %h1 #{@tag} に関連する投稿
  - @post.each do |post|
    = post.user.name
    = post.name
```















# 【 UIを整える （Tag-it） 】
- Tag-itは、タグ付け用UIを提供する jQueryのプラグイン。
- GitHub：[jquery-ui-rails](https://github.com/jquery-ui-rails/jquery-ui-rails)、[tag-it-rails](https://github.com/adorechic/tag-it-rails)、[tag-it](https://github.com/aehlke/tag-it)



## 1. 導入
1. [tag-it のGitHub](https://github.com/aehlke/tag-it)から、`Clone ` → `Download ZIP` をクリック。 
1. ファイルを解凍し、JSとCSSディレクトリ内に、格納。  
**・** cssフォルダ内の jquery.tagit.css 、 tagit.ui-zendesk.css → app/assets/stylesheets  
**・** jsフォルダ内の tag-it.js、tag-it.min.js → assets/javascripts
1. gem導入。

```rb:Gemfile
gem 'jquery-ui-rails'  # Tag-itは、 jQuery UI を使う
```

## 2. 設定
- Tag-it 、 jQuery UI を読み込むための設定。

```js:application.js
//= require jquery
//= require jquery_ujs
//= require jquery-ui
//= require tag-it
//= require_tree .

// turbolinks(ページ読み込みの高速化の役割)は、ページリロードしないとjQueryが発火しないので、削除(無効化)。
```
```scss:application.scss
@import "reset";
@import "font-awesome-sprockets";
@import "font-awesome";
@import "jquery.tagit";       // 記述の順番に注意
@import "tagit.ui-zendesk";   // こっちを後に書かないと、タグ削除（ x ）が表示されない
        :
```


## 3. tag-itを読み込む（jQuery）
- ページ更新で、tag-itイベントを発火させる。
- 入力ごとに、placeholderの表示を変更する↓↓
![Alt text](https://gyazo.com/4e02cda9fbc79af1e922707e954e5924/raw)  
※ ヘルプメッセージが表示された( .ui-helper-hidden-accessible クラスが メッセージ表示置き場に設定されてる模様 )ので、ひとまず、`display: none;`で、非表示にした。


```js:tag-itの読み込み（jQuery）
// ページ更新でtag-it発火
$(document).ready(function() {
  $(".tag_form").tagit({  // 指定のセレクタ( 今回は、:tag_list の text_field )に、tag-itを反映
    tagLimit:10,         // タグの最大数
    singleField: true,   // タグの一意性
 // availableTags: ['ruby', 'rails', ..]  自動補完する一覧を設定できる(※ 配列ならok)。今回は、Ajax通信でDBの値を渡す(後述)。
  });
  let tag_count = 10 - $(".tagit-choice").length    // 登録済みのタグを数える
  $(".ui-widget-content.ui-autocomplete-input").attr(
    'placeholder','あと' + tag_count + '個登録できます');
})

// タグ入力で、placeholder を変更
$(document).on("keyup", '.tagit', function() {
  let tag_count = 10 - $(".tagit-choice").length    // ↑ と同じなので、まとめた方がいい。
  $(".ui-widget-content.ui-autocomplete-input").attr(
  'placeholder','あと' + tag_count + '個登録できます');
});

  // 参:placeholderの書き換え方法
    $(".input").attr('placeholder','書き換え後のテキスト');
  // 参:placeholderの削除方法
    $(".input").removeAttr('placeholder');
```


- `$(セレクタ).tagit()`（jQuery）で、イベント発火すると、haml上は、  
**・** タグ入力フォーム(text_field)に、name属性: post[tag_list] が追加される( ※ post_params(コントローラー)で、**:tag_list** を許可してるので、タグ登録できるようになる )。id名、class名も追加される。  
**・** 入力フォーム内に、ul、li が追加される。


```haml:tagitイベント発火によるタグ入力フォームの変化(haml)
.input_form
  = f.text_field :tag_list, value: @post.tag_list.join(","), class: "tag_form tagit-hidden-field" name: "post[tag_list]" id: "post_tag_list"  # tagitイベントで、class名、name、id名が追加される
  -# tagitイベントで、追加される↓↓
  %ul.tagit.ui-widget.ui-widget-content.ui-corner-all
    %li.tagit-new
      = f.text_field, class: "ui-widget-content ui-autocomplete-input", autocomplete: "off", placeholder: "あと10個登録できます"                  # autocomplete="off" 自動入力の無効化
```






## 4. タグのインクリメンタルサーチ
- Ajax通信で、DBのデータをTag-itオプションの`availableTags`に渡してあげる。
![Alt text](https://gyazo.com/6f00948da1272468dab65e5ed6caa386/raw)

### 4-1. ルーティング
- 新規登録(id情報なし)、編集ページ(id情報あり)で、Ajax通信したい。
- htmlで取得する予定がないので、jsonをフォーマットにしとく。

```rb:routes.rb
  resources :posts, expect: [:index] do
    get 'get_tag_search', on: :collection, defaults: { format: 'json' }
    get 'get_tag_search', on: :member, defaults: { format: 'json' }
  end
```

### 4-2. コントローラー
- nameカラムが`params[:key]`から始まる、Tagsテーブルのレコードを全取得(※ :keyは、jQueryで定義した入力値。Ajaxで送ってるモノを取得)。

```rb:postsコントローラー
def get_tag_search
  @tags = Post.tag_counts_on(:tags).where('name LIKE(?)', "%#{params[:key]}%")
end
```

### 4-3. jbuilder
- コントローラーで定義した、＠tagsのnameカラムのみ取得。

```rb:views/posts/get_tag_search.json.jbuilder
json.array! @tags do |tag|
  json.name tag.name
end
  # [{name: "タグ名1"}, {name: "タグ名2"}, ..] の型で取得してる
```

### 4-4. jQuery
- タグフォームの入力値を取得し、Ajaxで送信 → コントローラーでDB検索 → jbuilderで所望データを取得 → jQueryでTag-itの availableTags に渡す。

```js:jQuery(Ajax通信部分)
$(document).on("keyup", '.tagit', function() {
           :
  // Ajaxで、タグ一覧を取得
  let input = $(".ui-widget-content.ui-autocomplete-input").val();  // 変数inputに、入力値を格納
  $.ajax({
    type: 'GET',
    url: 'get_tag_search',    // ルーティングで設定したurl
    data: { key: input },     // 入力値を:keyとして、コントローラーに渡す
    dataType: 'json'
  })

  .done(function(data){
    if(input.length) {               // 入力値がある時のみ 
      let tag_list = [];             // 空の配列を準備
      data.forEach(function(tag) {   // 取得したdataのnameを配列に格納
        tag_list.push(tag.name);     // 1つずつ追加。 tag_list = ["タグ名1", "タグ名2", ..]
      });
      $(".tag_form").tagit({
        availableTags: tag_list
      });
    }
  })
});
```