一直很想實作 tag 相關的功能,這項功能雖然在 blog 體系中相當常見但我一直覺得如果能更廣泛的應用,例如在 file management 上使用 tag system 會是一件想當棒的事情。

在現在常用的資料夾結構下,一個資料夾只能在一個分類目錄下面,但當某篇文章同時提到 rails 跟 javascript 的時候,如果把它放到 rails 資料夾下,當你想要找所有 javascript 的檔案的時候便可能遺漏,反之亦然,所以如果多加運用這樣的方式的話,也許可以讓很多事情方便不少。

啊!好像有點離題,接下來進入正題:

Tag Association

這裡我已經先建置好article modaltag modal

Tag 系統的好處在於一篇文同時可以有多個 tag 而一個 tag 也可以附著在多篇文章上面,所以在關聯上面應該是多對多的關係,這裡我選用的是has_and_belongs_to_many這個關聯。

所以分別在article modal加上:

class Article < ActiveRecord::Base
  has_and_belongs_to_many :tag
end

tag modal則加上:

class Tag < ActiveRecord::Base
  has_and_belongs_to_many :article
end

Create article_tag Table

但只有這樣是不夠的,多對多關聯還必須透過一個關聯的資料表去查詢,在has_and_belongs_to_many關係中,預設會去找一個由兩個 model 名稱由底線連結的資料表查詢關係,由本案例來說就是article_tag

所以新建一個 migration :

class CreateArticleTag < ActiveRecord::Migration
  def change
    #create article_tag table
    create_table :article_tag, id: false do |t|
      t.belongs_to :article, index: true
      t.belongs_to :tag, index: true
    end
  end
end
這裡不必使用 id 所以將它設為 false

然後下rake db:migrate,接下來問題是要如何儲存?

Create Checkboxes

首先在view/articles/new.html.erb(edit 也要加)加上:

<%= f.collection_check_boxes :tag_ids, Tag.all, :id, :name %>

collection_check_boxes非常完美地為我們實現了用 checkbox upadate 的功能。至於它是如何實現的,就讓我們實際檢視一下它所產生的 html :

<input type="checkbox" value="1" name="article[tag_ids][]" id="article_tag_ids_1">
<label for="article_tag_ids_1">thoughts</label>

<input type="checkbox" value="2" checked="checked" name="article[tag_ids][]" id="article_tag_ids_2">
<label for="article_tag_ids_2">work</label>

<input type="checkbox" value="3" name="article[tag_ids][]" id="article_tag_ids_3">
<label for="article_tag_ids_3">example</label>

<input type="hidden" name="article[tag_ids][]" value="">

這裡可以看出總共有三個 tag 分別為:

  • thoughts
  • work
  • example

這三個 tag 是由Tag.all產生出來的,而 value 的是由其中的tag.id產生, label 則是tag.name。而第一個參數,也是最重要的,是使用的方法,也就是@article.tag_ids。這個方法會回傳所有 tag 的id並以陣列方式呈獻,也可以傳入一個陣列用以 update ,而這裡正是用來更新。

另外一個值得注意的是,name欄位用的是article[tag_ids][],這個用意是為了讓收到的參數為params[:article][tag_ids]並且以陣列方式呈獻,如此直接匯入原本@article.update_attributes(article_params)即可,不必多做修改。

最後有疑問的點應該是:那最下面那段 hidden 欄位的用意是什麼?

其實很簡單,因為如果你希望刪除所有 tag ,通常你會把所有選項都解除勾選,但是 html 在面對完全沒被勾選的時候會什麼都不送出,也就是tag_ids欄位根本不會存在,所以欄位會原風不動,完全沒被更新到。所以 rails 在這裡幫你用 hidden 欄位送出一個空的元素,使得如果完全沒有勾選,就會更新成空的。

既然是使用update_attrbutes那一樣要注意 strong params 的問題,所以記得在將tag_ids整個陣列加入許可的參數中:
private
def article_params
  params.require(:article).permit({ tag_ids: [] })
end

Reference