acts-as-taggable-on と jQUery Tag-it でタグ付け機能作成


[acts-as-taggable-on](https://github.com/mbleigh/acts-as-taggable-on) を使って Rails アプリにタグ付け機能を追加する際に、UI には jQuery プラグインの [Tag-it](https://github.com/aehlke/tag-it) が良さそうなので使ってみた。この両者を合わせると良いよ、という記事はあったものの、具体的にどうしたら良いかまで書いてある記事を見つけられなかったので、せっかくなのでまとめてみる。

# 下準備

rails new して、タグ付けする Article モデルを scaffold で作成しておく。Rails のバージョンは 4.2.0 を使用。

```
rails new -T acts_as_taggable_on_sample
cd acts_as_taggable_on_sample
bin/rails g scaffold article title:string body:text
```
# acts-as-taggable-on の導入

[acts-as-taggable-on](https://github.com/mbleigh/acts-as-taggable-on) は ActiveRecord のモデルにタグを付けられるようにする gem。詳しい説明は省略するけど、モデルに対して tags や skills のように複数種類のキーワードでタグ付けのようなことをしたり、そのタグ（tags や skills）を複数モデルで使いまわしたりできる。タグクラウドの作成も簡単にできるらしい。

さっそく導入。

```Gemfile
gem 'acts-as-taggable-on', '~> 3.4'
```

```
bin/bundle
bin/rake acts_as_taggable_on_engine:install:migrations
bin/rake db:migrate
```
Article モデルでタグ付けできるように設定。

```app/models/article.rb
class Article < ActiveRecord::Base
  acts_as_taggable
end
```

するとこんなかんじでタグの追加、削除、取得ができる。

```irb
article = Article.new(title: 'title', body: 'body')

article.tag_list.add('Ruby', 'Rails', 'Rspec', 'Sinatra')
=> ["Ruby", "Rails", "Rspec", "Sinatra"]

irb(main):010:0> article.tag_list.remove('Rspec', 'Sinatra')
=> ["Ruby", "Rails"]

# 上書き。delimiter（デフォルトは ','）で分割してくれる。
article.tag_list = 'Haskell, OCaml, Lisp'
article.tag_list
=> ["Haskell", "OCaml", "Lisp"]

# add も parse オプションを渡すと分割してくれる
article.tag_list.add('Scala, JavaScript', parse: true)
=> ["Haskell", "OCaml", "Lisp", "Scala", "JavaScript"]

# すべての Article に付加されたタグ一覧を取得
Article.tags_on(:tags)
```

# Tag-it の導入

[Tag-it](https://github.com/aehlke/tag-it) はタグ付けの UI を提供する jQuery プラグイン。[ホームページのデモ](http://aehlke.github.io/tag-it/)を試すとよく分かる。

上記の GitHub レポジトリかホームページから js と css のファイルを落としてきて vendor/asssets 以下に配置する。

```terminal
curl https://raw.githubusercontent.com/aehlke/tag-it/master/js/tag-it.js -o vendor/assets/javascripts/tag-it.js

curl https://raw.githubusercontent.com/aehlke/tag-it/master/css/jquery.tagit.css -o vendor/assets/stylesheets/jquery.tagit.css

curl https://raw.githubusercontent.com/aehlke/tag-it/master/css/tagit.ui-zendesk.css -o vendor/assets/stylesheets/tagit.ui-zendesk.css
```

Tag-it は jQuery UI を利用するので、[jquery-ui-rails](https://github.com/joliss/jquery-ui-rails) を Gemfile に追加。

```Gemfile
gem 'jquery-ui-rails'
```

Tag-it と jQuery UI を読み込むように application.js と application.css を更新。

```app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
//= require jquery-ui
//= require tag-it
//= require turbolinks
//= require_tree .
```

```app/assets/stylesheets/application.css
/*
 *= require_tree .
 *= require jquery.tagit
 *= require tagit.ui-zendesk
 *= require_self
*/
```

ちなみに、tagit.ui-zendesk を後に書かないと、タグの削除アイコン（というか x という文字）が表示されないので注意。ちなみにちなみに、Tag-it のページで書かれているように jQuery UI Theme を使おうとしたところ、jQuery UI のアイコンが表示されなかった。[こんな stackoverflow の投稿](http://stackoverflow.com/questions/20561490/icons-not-showing-when-using-the-jquery-ui-rails-gem)があったけど今回は諦めた。

Scaffolding で作成された _form.html.haml の form_for ブロック内に ul 要素を作成して、適当な id をつける。

```app/views/articles/_form.html.haml
= form_for @article do |f|
  - if @article.errors.any?
  ...

  .field
    %ul#article-tags
  ...
```

その要素に対して tagit() を呼び出す。

```app/assets/javascript/articles.coffee
$(document).on 'ready page:load', ->
  $('#article-tags').tagit()
```

するとこんな感じで UI ができてしまった。

![](http://i.gyazo.com/137c63e963d23a7712b050e1b26f714a.png)

画面一番下に 'No search results.' という文字列が表示されてしまっているのだけど、これはオートコンプリート用のヘルプメッセージが表示されているらしい。この文字列は jQuery UI の .ui-helper-hidden-accessible クラスが付けられた、データ置き場のような div に置かれているので、css で表示にしてしまった。

```app/assets/stylesheets/articles.scss
.ui-helper-hidden-accessible {
  display: none;
}
```

# タグの保存

このまま Save ボタンを押してもタグは保存されないので、タグを保存できるようにしていく。

生成された DOM を見ると分かる通り、現在は入力したタグごとに `<input type="hidden" value="Rails" name="tags" class="tagit-hidden-field">` のような hidden な input フィールドができている。このままだとすべての name 属性が同じなので、controller に渡される params には 1 つしかタグが渡ってこない。また、params[:tags] に値が格納される事になるので、strong parameters で扱いづらい。

そこで、Tag-it の singleField オプションを有効にして input フィールドが 1 つになるようにし、fieldName オプションで name 属性を article[tag_list] に変更する。

```app/assets/javascript/articles.coffee
$(document).on 'ready page:load', ->
  $('.admin-articles #article-tags').tagit
    fieldName:   'article[tag_list]'
    singleField: true
```

すると input フィールドがこんな感じに。`<input type="hidden" style="display:none;" value="Rails,Ruby" name="article[tag_list]">`

controller 側では、strong parameters で許可するアトリビュートに :tag_list を追加。

```app/controllers/articles_controller.rb
def article_params
  params.require(:article).permit(:title, :body, :tag_list)
end
```

上の acts_as_taggable_on のにあった通り、tag_list にデフォルトの delimiter である ',' で区切られた文字列をセットすると、自動でタグを分割してくれる。

これで Save ボタンを押してみると、タグが保存されている！

```irb
Article.first.tag_list
=> ["Rails", "Ruby"]
```

# articles#edit でのタグの表示

タグの保存はできたものの、再度編集画面に言っても既存のタグが表示されていない。予めタグの編集 box にタグを追加するには、Tag-it の createTag メソッドを使うのだけど、それには Rails から JavaScript にタグを渡さないといけない。今回は [gon](https://github.com/gazay/gon) を使うことにした。gon は Rails から JavaScript にデータを渡すための gem。

```Gemfile
gem 'gon'
```

ここだけ erb だけど。。。

```app/views/layouts/application.html.erb
...
<%= include_gon %> # これを追加
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
...
```

controller で gon に対象 Article のタグをセット。

```app/controllers/articles_controller.rb
before_action  :set_article_tags_to_gon, only: [:edit]
...

private
...

def set_article_tags_to_gon
  gon.article_tags = @article.tag_list
end
```

それを Tag-it の createTag で追加していく。

```app/assets/javascript/articles.coffee
$(document).on 'ready page:load', ->
  $('.admin-articles #article-tags').tagit
    fieldName:   'article[tag_list]'
    singleField: true

  if gon.article_tags?
    for tag in gon.article_tags
      $('#article-tags').tagit 'createTag', tag
```

これで、編集画面で既存のタグが表示される。

# オートコンプリート

タグのオートコンプリートもできる。gon を介して既存の全タグを tagit() の availableTags オプションに渡すだけ。

```app/controllers/articles_controller.rb
def set_available_tags_to_gon
  gon.available_tags = Article.tags_on(:tags).pluck(:name)
end
```

```app/assets/javascript/articles.coffee
$(document).on 'ready page:load', ->
  $('.admin-articles #article-tags').tagit
    fieldName:     'article[tag_list]'
    singleField:   true
    availableTags: gon.available_tags
```

オートコンプリートできるようになりました。

![](http://i.gyazo.com/a638038776fb8d61035a09f047187490.png)

なお、今のままだと articles#new や #edit 以外でも gon.available_tags を読み込んで失敗してしまう。今回はサンプルなので放置。

# タグの表示

articles#index と #show でタグを表示する。といっても、こちらはただ view を作ればよいだけ。


N + 1 問題回避のために index では tags を includes しておくことだけ忘れずに。tag_list でなく tags にしたのは、tag_list だとタグの利用回数も取得できて良いかなぁと思ったので。

```app/controllers/articles_controller.rb
def index
  @articles = Article.all.includes(:tags)
end
```

そしてこんなかんじのパーシャルを用意して

```app/views/articles/_tags.html.haml
- tags.each do |tag|
  = "#{tag.name} (#{tag.taggings_count})"
```

index.html.haml や show.html.haml の適当なところで `render partial: 'tags', locals: { tags: article.tags }` みたいにして render すると、こんなかんじでタグが利用回数とともに表示される。

![](http://i.gyazo.com/cac3f299b7063c28891b0d878559ec44.png)

# タグで検索

タグで Article を検索する機能を作る。まずはパーシャルで表示していたタグをリンクにして、GET パラメータとしてタグの name を埋め込む。

```app/views/articles/_tags.html.haml
- tags.each do |tag|
  = link_to "#{tag.name} (#{tag.taggings_count})", articles_path(tag: tag.name)
```

controller では、acts-as-taggalbe-on が提供する tagged_with という scope で記事を絞り込む。

```app/controllers/articles_controller.rb
def index
  @articles = params[:tag].present? ? Article.tagged_with(params[:tag]) : Article.all
  @articles = @articles.includes(:tags)
end
```

するとこんなかんじで Article を絞り込めるようになる。

![](http://i.gyazo.com/3fcdbdd168a6871e9f138cdcb79686cd.png)

Ruby(1) というリンクを検索すると・・・

![](http://i.gyazo.com/4b52d8c6e299e29de7ebe5b92775c2a2.png)

# 最後に

今回作ったアプリは[こちら](https://github.com/tyamagu2/acts_as_taggable_on_sample)。途中書いたとおり、特に JS 周りは適当なのでご留意下さい。まだ見直しできてなくて他にもおかしいところがあるかもしれないので、お気づきの方はご指摘下さい。

acts-as-taggable-on はここに書いた以上にもっと色々出来るっぽいので GitHub のページを読むことをおすすめします。色々探していたら[タグクラウドの作成方法の記事](http://morizyun.github.io/blog/acts-as-taggable-on-gem-rails/)もありました。

# 余談

久々に turbolinks 使ったら、$(document).ready() じゃダメなこと忘れていてハマった。[Turbolinksをオフしないためにやった事](http://qiita.com/saboyutaka/items/bcc0966313c6f7399a6e)を読んで何やったら良いか思い出した。