Last Updated: February 25, 2016
·
9.193K
· sebastialonso

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!

3 Responses
Add your response

I'm implementing select2 tagging with a has_many through relationship. My implementation has 2 different scenarios.

  1. The select menu allows multiple (tagging) but does not allow on the fly input into the select menu.
  2. 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: "repair
type_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: "repair
type_ids"=>["5688,5690"]

Are you able to comment on this? Have you ran into this problem?

over 1 year ago ·

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?

over 1 year ago ·

"I'll leave that to you to find out." Noooooooooooooo !!!!!!

over 1 year ago ·