我最近为此写了blog post;为简洁起见,我的方法允许您拥有(可选的)context 过滤标签(例如,按模型和模型上的属性),而 @Tim Santeford 的解决方案将为您提供所有标签模型(未按字段过滤)。以下是逐字帖。
我尝试了Tim Santeford's solution,但问题出在标签结果上。在他的解决方案中,您会通过自动完成功能返回所有个现有标签,而不是限制在您的模型和可标记字段中!所以,我想出了一个在我看来要好得多的解决方案;它可以自动扩展到您想要标记的任何模型,它很高效,最重要的是它非常简单。它使用 acts-as-taggable-on gem 和 select2 JavaScript 库。
安装 Acts-As-Taggable-On gem
- 将作为可标记的行为添加到您的 Gemfile:
gem 'acts-as-taggable-on', '~> 3.5'
- 运行
bundle install进行安装
- 生成必要的迁移:
rake acts_as_taggable_on_engine:install:migrations
- 使用
rake db:migrate 运行迁移
完成!
在 MVC 中设置常规标记
假设我们有一个Film 模型(因为我有)。只需将以下两行添加到您的模型中:
class Film < ActiveRecord::Base
acts_as_taggable
acts_as_taggable_on :genres
end
模型就是这样。现在进入控制器。您需要接受参数中的标签列表。所以我的FilmsController 中有以下内容:
class FilmsController < ApplicationController
def index
...
end
...
private
def films_params
params[:film].permit(..., :genre_list)
end
end
请注意,参数不是我们在模型中指定的genres。不要问我为什么,但是acts-as-taggable-on 需要 singular + _list,而这正是视图中所要求的。
到视图层!我使用SimpleForm gem 和Slim 模板引擎来查看视图,因此如果您不使用 gem,我的表单可能看起来与您的有所不同。但是,它只是一个普通的文本输入字段:
= f.input :genre_list, input_html: {value: @film.genre_list.to_s}
您需要设置了该值的 input_html 属性,以便将其呈现为逗号分隔的字符串(这是控制器中预期的可标记行为)。当您提交表单时,标记现在应该可以工作了!如果还不行,我推荐观看(精彩)Ryan Bates' Railscast episode on tagging。
将 select2 集成到您的表单中
首先,我们需要包含 select2 库;您可以手动包含它,也可以使用(我的偏好)select2-rails gem,它为 Rails 资产管道打包 select2。
将 gem 添加到您的 Gemfile:gem 'select2-rails', '~> 4.0',然后运行 bundle install。
在您的资产管道中包含 JavaScript 和 CSS:
在 application.js 中://= require select2-full。在 application.css 中:*= require select2.
现在您需要稍微修改一下您的表单以包含 select2 对标记的期望。这似乎有点令人困惑,但我会解释一切。更改您之前的表单输入:
= f.input :genre_list, input_html: {value: @film.genre_list.to_s}
到:
= f.hidden_field :genre_list, value: @film.genre_list.to_s
= f.input :genre_list,
input_html: { id: "genre_list_select2",
name: "genre_list_select2",
multiple: true,
data: { taggable: true, taggable_type: "Film", context: "genres" } },
collection: @film.genre_list
我们添加一个 隐藏 输入,它将作为发送到控制器的真实值。 Select2 返回一个数组,其中acts-as-taggable-on 需要一个逗号分隔的字符串。 select2 表单输入在其值更改时转换为该字符串,并设置为隐藏字段。我们很快就会解决这个问题。
f.input 的 id 和 name 属性实际上并不重要。它们不能与您的 hidden 输入重叠。 data 哈希在这里非常重要。 taggable 字段允许我们使用 JavaScript 一次性初始化所有 select2 输入,而不是通过 id 手动初始化每个输入。 taggable_type 字段用于过滤特定型号的标签,context 字段用于过滤之前在该字段中使用过的标签。最后,collection 字段只是在输入中适当地设置值。
下一部分是 JavaScript。我们需要在整个应用程序中初始化所有 select2 元素。为此,我只需将以下函数添加到我的 application.js 文件中,以便它适用于每条路线:
// Initialize all acts-as-taggable-on + select2 tag inputs
$("*[data-taggable='true']").each(function() {
console.log("Taggable: " + $(this).attr('id') + "; initializing select2");
$(this).select2({
tags: true,
theme: "bootstrap",
width: "100%",
tokenSeparators: [','],
minimumInputLength: 2,
ajax: {
url: "/tags",
dataType: 'json',
delay: 100,
data: function (params) {
console.log("Using AJAX to get tags...");
console.log("Tag name: " + params.term);
console.log("Existing tags: " + $(this).val());
console.log("Taggable type: " + $(this).data("taggable-type"));
console.log("Tag context: " + $(this).data("context"));
return {
name: params.term,
tags_chosen: $(this).val(),
taggable_type: $(this).data("taggable-type"),
context: $(this).data("context"),
page: params.page
}
},
processResults: function (data, params) {
console.log("Got tags from AJAX: " + JSON.stringify(data, null, '\t'));
params.page = params.page || 1;
return {
results: $.map(data, function (item) {
return {
text: item.name,
// id has to be the tag name, because acts_as_taggable expects it!
id: item.name
}
})
};
},
cache: true
}
});
});
这可能看起来很复杂,但并不难。基本上,$("*[data-taggable='true']") 选择器只会获取我们在数据中设置了taggable: true 的每个 HTML 元素。我们刚刚将它添加到表单中,这就是为什么 - 我们希望能够为所有 taggable 字段初始化 select2。
剩下的只是与 AJAX 相关的代码。本质上,我们使用参数name、taggable_type 和context 对/tags 进行AJAX 调用。听起来有点熟?这些是我们在表单输入中设置的数据属性。当返回结果时,我们只需给 select2 标签的名称。
现在你可能在想:我没有/tags 路由!。你说得对!但你即将:)
添加 /tags 路由
进入您的routes.rb 文件并添加以下内容:resources :tags。您不必为标签添加 all 路线,但我这样做是为了让我可以轻松地添加 CRUD 标签。你也可以这样做:get '/tags' => 'tags#index'
这确实是我们目前需要的唯一路线。现在我们有了路由,我们必须创建一个名为 TagsController 的控制器:
class TagsController < ApplicationController
def index
@tags = ActsAsTaggableOn::Tag
.where("name ILIKE ?", "%#{params[:name]}%")
.where.not(name: params[:tags_chosen])
.includes(:taggings)
.where(taggings: {taggable_type: params[:taggable_type]})
@tags = @tags.where(taggings: {context: params[:context] }) if params[:context]
@tags.order!(name: :asc)
render json: @tags
end
end
这很简单。我们可以向/tags 发送请求,参数为name(标签文本)、tags_chosen(现有选定标签)、taggable_type(被标记的模型),和可选的context(被标记的字段)。如果我们有“喜剧”和“阴谋”的类型标签,然后在我们的表单中输入 co,呈现的 JSON 应该是这样的:
[
{
"id": 12,
"name": "comedy",
"taggings_count": 1
},
{
"id": 11,
"name": "conspiracy",
"taggings_count": 1
}
]
现在在 select2 输入中,您应该看到“喜剧”和“阴谋”作为自动完成的标签选项!
我的标签不会保存!
还有最后一步。我们需要将 select2 值设置到我们之前创建的 hidden 字段中。
此代码对您而言可能会有所不同,具体取决于您构建表单的方式,但您基本上希望获取 select2 输入,将字符串数组转换为 CSV 字符串(例如 ["comedy", "conspiracy"] --> "comedy, conspiracy"),并将该 CSV 字符串设置为隐藏字段。幸运的是,这并不太难。
您可以捕获 select2 输入更改事件,或任何其他适合您的事件。这是您的选择,但必须执行此步骤以确保 Rails 控制器正确接收该值。同样,在 application.js 中:
/*
* When any taggable input changes, get the value from the select2 input and
* convert it to a comma-separated string. Assign this value to the nearest hidden
* input, which is the input for the acts-on-taggable field. Select2 submits an array,
* but acts-as-taggable-on expects a CSV string; it is why this conversion exists.
*/
$(document).on('select2:select select2:unselect', "*[data-taggable='true']", function() {
var taggable_id = $(this).attr('id')
// genre_list_select2 --> genre_list
var hidden_id = taggable_id.replace("_select2", "");
// film_*genre_list* ($= jQuery selectors ends with)
var hidden = $("[id$=" + hidden_id + "]")
// Select2 either has elements selected or it doesn't, in which case use []
var joined = ($(this).val() || []).join(",");
hidden.val(joined);
});
成功转换值后,您应该在控制器操作中看到以下内容:"genre_list"=>"comedy,conspiracy"
这就是您在 Rails 中使用acts-as-taggable-on 和 select2 自动完成标签所需要做的一切!