Tag
name:string counter:integer
tag:references table:references record_id:integer
Model
has_many: tags
to table.rb, make sure record_id is nil for table tags. Define tag_list
and tag_list=
methods tags, tag_list, tag_list=
function to record.rb# models/table.rb
has_many :taggings
# keep tagging order
has_many :tags, -> {Tagging.where(record_id: nil).order(id: :ASC)}, through: :taggings attr_accessor :tag_list
def tag_list
tags.map(&:name)
end
# Array or String, e.g. "tag1, tag2, tag3"
def tag_list=(names)
tag_names = [];
if names.present?
names = names.split(',') if names.instance_of?(String)
tag_names = names.collect(&:strip).reject(&:blank?)
end
self.tags = tag_names.map do |n|
Tag.where(name: n.strip).first_or_create!
end
end
# models/record.rb
def tags
Tag.joins(:taggings).where({taggings: {table: self.table, record_id: self.id}).order("taggings.id": :ASC)
end
def tag_list
self.tags.map(&:name)
end
def tag_list=(names)
tag_names = []
if names.present?
names = names.split(',') if names.instance_of?(String)
tag_names = names.collect(&:strip).reject(&:blank?)
end
tags = tag_names.map do |n|
Tag.where(name: n).first_or_create!
end
# Create or update new tags
taggings = tags.map do |tag|
Tagging.where(tag:tag,table:self.table,record_id:self.id).first_or_create!
end
# Delete tags not used anymore
tids = taggings.pluck(:id);
Tagging.where(table:self.table,record_id:self.id).where.not(id: tids).delete_all
end
Controller & Views
tag_list
filed to table form. Such that use can input tag list as tag1, tag2 ...
Tag Cloud
Follow guide
Define tag_count in models
class Tag < ApplicationRecord
has_many :taggings
has_many :tables, through: :taggings
validates_presence_of :name
def to_param
return self.name || self.id
end
def self.tag_counts
Tag.select("tags.id, tags.name,count(taggings.tag_id) as count").joins(:taggings).group("taggings.tag_id, tags.id, tags.name")
end
end
define tag_cloud in helper/application.rb
def tag_cloud(tags, classes)
max = tags.sort_by(&:count).last
tags.each do |tag|
index = tag.count.to_f / Integer(max.count) * (classes.size - 1)
yield(tag, classes[index.round])
end
end
Define css in javascript/stylesheets/application.scss
.tag-cloud-1 { font-size: 1.0em;}
.tag-cloud-2 { font-size: 1.2em;}
.tag-cloud-3 { font-size: 1.4em;}
.tag-cloud-4 { font-size: 1.6em;}
.tag-cloud-5 { font-size: 1.8em;}
.tag-cloud-6 { font-size: 2.0em;}
show in the views
<div class="tags-cloud glassy-bg">
<% tag_cloud Tag.tag_counts, %w{tag-cloud-1 tag-cloud-2 tag-cloud-3 tag-cloud-4 tag-cloud-5 tag-cloud-6} do |tag, css_class| %>
<%= link_to "#{tag.name}(#{tag.count})", tag_path(tag.name), class: css_class %>
<% end %>
</div>
Load More
tag_controller
def show
@limit = params[:limit] || 20;
@section=params[:section] # operation, also section ID
@record = params[:record];
cond = {}
cond[:id] = (0..@record.to_i-1) if @record
if @section.blank? || @section=="table"
@tables =@tag.tables.where(cond).order(id: :DESC).limit(@limit)
end
if @section.blank? || @section=="record"
@taggings = Tagging.where(tag: @tag).where.not(record_id: nil).where(cond).order(id: :DESC).limit(@limit)
@records = @taggings.map {|r| Table.find(r.table_id).find_record(r.record_id)};
end
respond_to do |format|
format.html
format.js
end
end
show.js.erb
<% if @section.present? %>
<% if @section=="table"%>
<% if @tables.empty? %>
$('#<%=@section%> .load-more-button').hide()
<% else %>
$('#<%=@section%> .load-more-records').append('<%= escape_javascript(render(partial: "tables/table_card",collection: @tables)) %>')
<% end %>
<% elsif @section=="record" %>
<% if @records.empty? %>
$('#<%=@section%> .load-more-button').hide()
<% else %>
$('#<%=@section%> .load-more-records').append('<%= escape_javascript(render("tags/tag_records", taggings: @taggings, records: @records)) %>')
<% end %>
<% end %>
<% else %>
// Nothing, reload
<% end %>
views.
Note that we using tagging ID instead of record ID for record load more.
<% tabs = [:table, :record] %>
<ul class="nav nav-tabs nav-pills" role="tablist">
<% tabs.each_with_index do |opt,idx| %>
<% cls = idx==0 ? "active" : "" %>
<% select = (idx==0) %>
<li class="nav-item">
<a class="nav-link <%=cls%>" id="<%=opt%>-tab" href="#<%=opt%>" data-toggle="tab" role="tab" aria-controls="<%=opt%>" aria-selected="<%= select %>"><%=t("models.#{opt.to_s.capitalize}")%></a>
</li>
<% end %>
</ul>
<div class="tab-content mt-2">
<% tabs.each_with_index do |opt,idx| %>
<% cls = (idx==0) ? "active" : "" %>
<div class="tab-pane fade show <%=cls%>" id="<%=opt%>" role="tabpanel" aria-labelledby="<%=opt%>-tab">
<div class="load-more-records">
<% if(opt==:table) %>
<%= render partial: "tables/table_card", collection: @tables %>
<% else %>
<%= render "tags/tag_records", taggings: @taggings, records: @records %>
<% end %>
</div>
<div class="load-more-button text-center mt-2">
<%= image_tag "ajax-loader.gif", style: "display:none;", class: "loading-gif" %>
<%= link_to t('views.load_more'), "#", class: "load-more"%>
</div>
</div>
<% end %>
</div>
// _tag_records.html.erb
<% taggings.zip(records).each do |tagging,record| %>
<div class="record" id="tagging-<%=tagging.id%>" data-id="<%=tagging.id%>">
<%= render "records/record", record: record %>
</div>
<% end %>
AJAX auto-completion
Install select2
yarn add select2
yarn add select2-bootstrap-theme
Apply JS
// javascript/packs/application.js
import 'select2'
import 'select2/dist/css/select2.min.css'
import "select2-bootstrap-theme/dist/select2-bootstrap.min.css";
// javascript/packs/tag.js
import $ from 'jquery' // must import jquery
$(document).on("turbolinks:load", function() {
$('.tag-input').select2({
tags: true,
multiple: true,
theme: 'bootstrap',
//tokenSeperators: [','],
placeholder: 'Please enter your tags',
minimumInputLength: 2,
ajax: {
url: "/tags.json",
dataType: 'json',
data: function (params) {
return { term: params.term };
},
processResults: function (tags) {
console.log(tags)
var tlist = $.map(tags, function (tag) {
return { id: tag.name, text: tag.name }
});
console.log(tlist)
return {results: tlist}
}
}
});
});
Tag search
def index
cond = "";
if params[:term].present?
cond = "name LIKE '%#{params[:term]}%'"
end
@tags = Tag.where(cond).order(id: :DESC)
respond_to do |format|
format.html
format.json
end
end
Apply in views. Note, must use select_tag, text_tag not work
<%= form.select :tag_list, form.object.tag_list, {}, {multiple: true, class: "form-control tag-input"} %>
// OR
<% opt = options_for_select(@record.tag_list, @record.tag_list) %>
<%= select_tag :tag_list, opt, {multiple: true, class: "form-control tag-input"} %>
Issues
When submit form, blank value added automatically.
"tag_list"=>["", "adafa", "1111"]
Solution: add option to select include_hidden: false
. See here
<%= form.select :tag_list, form.object.tag_list, {include_hidden: false}, {multiple: true, class: "form-control tag-input"} %>
DO NOT do this. This will make update tag list to empty not working. As we don't know it's emptying or keeping the tag list.
UI is ugly is using bootstrap
style
Need to install select2-bootstrap-theme
guide
i18n
Failed. Seems i18n.js has syntax error. I updated the file and it passes. See here for details
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd){
Still can't work. language: $('html').attr('lang')
or language: 'zh-CN'
still not work.
References