54
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

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

Alt text

環境

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

【 タグ付け機能 : 登録/削除/表示 】

1. gem導入

Gemfile
gem 'acts-as-taggable-on', '~> 6.0'
ターミナル
% 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のバグ?)
  • マイグレーションファイルを修正。
    ※ 外部キーを削除せずに、インデックスを削除しよーとしてるのが原因みたいなので、インデックス削除前に外部キー部分をコメントアウトで対応。
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
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)
例)タグ検索
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. モデル(アソシエーション)

  • タグ付けしたいモデルに、アソシエーションを追加。
Postモデル
acts_as_taggable   # acts_as_taggable_on :tags の省略

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

3. コントローラー

  • タグ登録のため、ストロングパラメーターに、:tag_listを追加。
  • タグ表示のため、アクションも追加。
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)
- 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
タグの表示(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ページに、関連する投稿一覧を表示する。
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)
- @tags.each do |tag|
  = link_to "#{tag.name}(#{tag.taggings_count})", posts_path(tag: tag.name)
タグに紐付く投稿一覧の表示(haml)
- if @post.present?
  %h1 #{@tag} に関連する投稿
  - @post.each do |post|
    = post.user.name
    = post.name

【 UIを整える (Tag-it) 】

1. 導入

  1. tag-it のGitHubから、Clone Download ZIP をクリック。
  2. ファイルを解凍し、JSとCSSディレクトリ内に、格納。
    cssフォルダ内の jquery.tagit.css 、 tagit.ui-zendesk.css → app/assets/stylesheets
    jsフォルダ内の tag-it.js、tag-it.min.js → assets/javascripts
  3. gem導入。
Gemfile
gem 'jquery-ui-rails'  # Tag-itは、 jQuery UI を使う

2. 設定

  • Tag-it 、 jQuery UI を読み込むための設定。
application.js
//= require jquery
//= require jquery_ujs
//= require jquery-ui
//= require tag-it
//= require_tree .

// turbolinks(ページ読み込みの高速化の役割)は、ページリロードしないとjQueryが発火しないので、削除(無効化)。
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
    ※ ヘルプメッセージが表示された( .ui-helper-hidden-accessible クラスが メッセージ表示置き場に設定されてる模様 )ので、ひとまず、display: none;で、非表示にした。
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 が追加される。
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

4-1. ルーティング

  • 新規登録(id情報なし)、編集ページ(id情報あり)で、Ajax通信したい。
  • htmlで取得する予定がないので、jsonをフォーマットにしとく。
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で送ってるモノを取得)。
postsコントローラー
def get_tag_search
  @tags = Post.tag_counts_on(:tags).where('name LIKE(?)', "%#{params[:key]}%")
end

4-3. jbuilder

  • コントローラーで定義した、@tagsのnameカラムのみ取得。
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 に渡す。
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
      });
    }
  })
});
54
56
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?