- タグ付け機能を実装時の記録。エラーで戸惑ったので、備忘録として。
- acts-as-taggable-on(gem)、UI に Tag-it( jQueryのプラグイン )を使用。
環境
- 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 通りやると、マイグレーションエラー発生。
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. タグの表示
タグの表示(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) 】
- Tag-itは、タグ付け用UIを提供する jQueryのプラグイン。
- GitHub:jquery-ui-rails、tag-it-rails、tag-it
1. 導入
-
tag-it のGitHubから、
Clone
→Download ZIP
をクリック。 - ファイルを解凍し、JSとCSSディレクトリ内に、格納。
・ cssフォルダ内の jquery.tagit.css 、 tagit.ui-zendesk.css → app/assets/stylesheets
・ jsフォルダ内の tag-it.js、tag-it.min.js → assets/javascripts - 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の表示を変更する↓↓
※ ヘルプメッセージが表示された( .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. タグのインクリメンタルサーチ
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
});
}
})
});