Sometimes, you just need to add tagging to a form on your website. At work, we (Freddy and I) recently had to implement some tagging for fields that had a pre-defined list of options to choose from. Taking advantage of Rails 4.x’s and PostgreSQL’s native support for array-type columns, we were able to avoid the hassle of extra models, join tables, foreign keys, etc. We then used Chosen.js to make the UI clean and dead-simple.

Here’s how it all came together.

1. Let’s add our column to the DB.

Let’s start by adding a column to our Person model - we’ll call it states_visited.


# in your console
rails g migration add_states_visited_to_people

Open up that migration file and let’s make it look like this:


# db/migrate/XYZ_add_states_visited_to_people.rb
class AddStatesVisitedToPeople < ActiveRecord::Migration
  def change
    add_column :people, :states_visited, :text, array: true, default: []
  end
end

And of course, run the migration: rake db:migrate

2. Now set up the column in your strong parameters.

Because ActiveRecord now expects an array, we need to let strong_parameters know what to permit, so we’ll tweak our UsersController like so:


# app/controllers/people_controller.rb
def person_params
  params.require(:person).permit(:name, { states_visited: [] })
end

The key bit here is { states_visited: [] }, which gives strong_parameters a heads up that we’re going to be submitting the data for that column as an array.

3. Set up your form helper

Actually, before we can do that, I’m going to set up a constant that contains an Array of the names of the 50 United States of America to reference in our form helper a little later.


# config/initializers/states.rb
MURICA = ["Alabama", "Alaska", ..., "Wyoming"]

(You’ll need to restart after you add this)

Great. Now the semantic way to mark this up would be with a multiple-select tag - we have a list of options to select from, and you can select multiple options from that list. So lets do that.


# app/views/people/show.html.erb
<%= form_for @user do |f| %>
    # stuff...
    
    <%= f.label :name, 'Your Name' %>
    <%= f.input :name, placeholder: 'John/Jane Doe' %>
    
    <%= f.label :states_visited, 'States You\'ve Been To' %>
    <%= f.select :states_visited, MURICA, {}, { multiple: true, class: 'taggable' } %>
<% end %>

Believe it or not, that’s all we need to get it working. But as far as I know, not a single person on earth enjoys using multiple-select fields when filling out a form, so we’re going to style it up a bit.

4. Implement Chosen.js

Chosen is a great little jQuery plugin for easily and vastly improving the UI of your selects and multiple-selects. Since we’re using Rails, we’ll just add the chosen-rails gem to our gemfile and run bundle to install it, and then restart your app again.

We’ll also want to include it in our application.js manifest (I’m using Coffeescript here):


# app/assets/javascripts/application.js
//= require chosen-jquery

And likewise to our stylesheet manifest:


# app/assets/stylesheets/application.css
*= require chosen

Now it’s just a matter of initializing Chosen on our select elements, which we setup with a class of “taggable”:


# app/assets/javascripts/forms.js
jQuery ->
  $('.taggable').chosen()

Well that was easy. I’ll leave it to you to research the options available in the Chosen documentaiton, but with those two lines, we go from this:

demo_boring_select

…to this:

demo_exciting

Now let’s get our view to display our new fancy tags. You can do this in a similar way you might with any other standard Rails association:


# app/views/people/show.html.erb

Hi! My name is <%= @person.name %>!

States I've Been To:

<% if @person.states_visited.any? %>
    <% @person.states_visited.each do |state| %>
  • <%= state %>
  • <% end %>
<% end %>

5. ???

But wait! There’s a problem. The rails helper for multiple-select elements introduces a combination feature/bug that will complicate things a bit. Rails will add a hidden input just before your select tag that will explicitly set your states_visited attribute to nil in the event that you deselect any previously selected options. This is similar to Rails' checkbox helper - HTML will not send form data for a given attribute if no options (or checkboxes) are selected (or checked). This hidden input forces the nil value to be sent with the form, thus updating your record appropriately.

However, in this case, the hidden input is being included as an element in our array of states_visited, making the first entry in our list a big fat blank, regardless of other values.

demo_bug

To get around this, we’re going to follow the advice gleaned from this StackOverflow answer, and set up a private method in our ApplicationController that will strip blank items from any array-type parameters before doing anything with the params:


# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :clean_select_multiple_params, only: [:create, :update]

  # existing code...

  private

  # Used to strip blank first values from array-type params.
  def clean_select_multiple_params hash = params
    hash.each do |key, value|
      case value
      when Array then value.reject!(&:blank?)
      when Hash then clean_select_multiple_params(value)
      end
    end
  end
end

This method loops through the params hash, checking if a given parameter is an Array or a Hash (for Postgres’s h-store column types), and rejects any blank values contained therein.

By putting this in ApplicationController and calling it in a before_action filter we’re making it happen on any future controllers that have #create and #update methods. If we later decide we want to implement tagging on any other models, we won’t have to worry about this feature/bug any more.

(The other answers on SO seem to favor adding a similar method to our Person model. This would require duplicating that method in any other models that might utilize tagging. The way I see it, it’s the controller’s job to serve as the gatekeeper of data passing between the view and the model.)

6. Profit

Speaking of DRY, since we’ve bound our Chosen.js initializer to anything with a class of “taggable”, you can now rinse and repeat steps 1, 2, and 3 on any additional columns you want, and fill your whole app with taggable inputs. Chosen.js offers great customizations involving grouped selects and more, so go check it out and customize to suit your needs.

The full working code for this tutorial is available on GitHub, and I encourage you to clone it and test it out for yourself.

The end.