Many to many in Rails: Tagging with Select2
Select2 is an excellent tool for customizing select boxes.
The option
elements can be filtered on the fly, categorized and styled beautifully.
Moreover, when dealing with many-to-many relation, and specially with HABTM relation, selecting the value for a field can be a pain in the ass, depending of the number of different options you can choose from. Having a list that extends far far down the page is not aesthetic nor practical.
To me Select2 takes the prize among other Jquery plugin options like Chosen and Token Input. These two options are covered in the revised version of Railscasts' episode 258. Personally, Select2's documentation is not the friendly and implement-in-one-step doc that I'd write, so this tip will focus on that.
First things first, our two models: Thing and Tag. This guide assumes you have already install Select2 in your Rails application (look the select2-rails gem)
class Thing < ActiveRecord::Base
has_and_belongs_to_many :tags
attr_reader :tag_tokens
def tag_tokens=(tokens)
self.tag_ids = Tag.ids_from_tokens(tokens)
end
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :things
def self.ids_from_tokens(tokens)
tokens.gsub!(/<<<(.+?)>>>/) { create!(name: $1.capitalize).id }
tokens.split(',')
end
end
Code Explanation
- In Thing.rb
The attr_reader
lets me play with the virtual attribute tag_tokens
. We say is a virtual attribute because is not actually part of the model, it doesn't show up in the Schema.
The function tag_tokens=
is a setter method for tagtokens virtual attribute, or (more correctly) the `tagids` attribute.
- In Tag.rb
I will explain this function later. For the moment, let'say it does a split on a string based on the comma character.
The view when I'll be using Select2 looks like:
#textos/show.html.erb
<%= form_for(@thing) do |f| %>
<%= f.text_field :name %>
<%= f.text_field :tag_tokens %>
<% end %>
So, let me tell you what I would like to do. I'd like the select to show me all the Tags I already have, filter them as I write, but also, if I can't find a Tag, I should be able to create it right from there.
The select2 object is then initialized:
#textos.js.coffee
$("#thing_tag_tokens").select2
multiple: true
data: window.tags
createSearchChoice: (term, data) ->
if $(data).filter(->
@text.localeCompare(term) is 0
).length is 0
id: "<<<" + term + ">>>"
text: term
Code Explanation
All the options can be found in the Select2 documentation, but I should probably talk about the window.tags
object.
I initialize the data of the Select2 object with the instances of Tag I already have. I perform an Ajax call to retrieve all the Tags, but with a detail. Select2 expects objects with the id
and text
fields, so window.tags
is an array of objects with those fields, where text
holds the name of the tag. I'll leave that to you to find out.
The createSearchChoice creates a new tag inside the Select2 element, namely something like
{ id: "<<<term>>>", text: "term" }
But it's not in our database yet. We need to catch this uncreated tag in the controller and somehow create it in our Tag table. And that's precisely what the first line of the self.ids_from_tokens
function, in Tag.rb, does.
Finally, if you're using Rails 4, you should permit the field tag_tokens
in your thing_params
Let me know if you found anything weird!
Written by Sebastián González
Related protips
3 Responses
I'm implementing select2 tagging with a has_many through relationship. My implementation has 2 different scenarios.
- The select menu allows multiple (tagging) but does not allow on the fly input into the select menu.
- Same as above but uses ajax to allow the user to enter new select values on the fly.
Scenario 1 works well for tagging. Scenario 2 seems to work but does not save the values. My problem seems to come down to my my input elements for the scenarios.
Scenario 1 uses:
<%= f.association :repairers, labelmethod: :repname, valuemethod: :id, includeblank: true, label: 'Repairer'%>
and when the form is submitted gives params similar to:
"repairer_ids"=>["", "1132", "1131"]
Scenario 2 uses:
<%= f.hiddenfield :repairtypeid, :class => "required on-the-fly-select select"%>
and uses a lot of js code to implement on the fly input for the select menu. When the form is submitted the data will look like: "repairtype_id"=>"5688,5690"
So with scenario 2 the ids are not submitted as an array. I have tried changing the select to: <%= hiddenfieldtag("repairitem[repairtypeids][]", "", :id => "repairitemrepairtypeids", :class => "required on-the-fly-select select") %>
but then the relevant param is submitted as one array: "repairtype_ids"=>["5688,5690"]
Are you able to comment on this? Have you ran into this problem?
I don't understand this part:
uses a lot of js code to implement on the fly input for the select menu
Are you using custom javascript for input and select2 at the same time?
Anyway, getting the ids as an array is not the desired behaviour? What exactly are you aiming to receive from the input?
"I'll leave that to you to find out." Noooooooooooooo !!!!!!