Scenrario:

We have the models Post, User and Group. Post can be owned by either by an User or by a Group. This makes Post a perfect candidate for ActiveRecord polymorphic association and once the simple configurations are done, it’s all magic - @post.owner (assuming you named the relationship as owner) gives to the owner object - either User or Group object. @user.posts and @group.posts gives you their respective posts.

Unfortunately Rails built-in associations and form helpers don’t solve the problem when it comes to handling the user inputs though html forms. For example: Assume that you have a form that allows you to create Post with title and owner fields. Since owner is a polymorphic association, you will ideally want a single select box with list of users and groups to choose from but this is possible with a combination of a few helpers.


Let’s dive in

The code for this rails app is available at https://github.com/opendrops/rails_globalid_polymorphic_form

# Creating our playground rails app
rails new globalid_polymorphic

# Scaffolding the models we require
rails g scaffold Group name
rails g scaffold User name
rails g scaffold Post title owner:references

Since Rails does not have a generator for polymorphic association, we need to modify the generated migration file for create_posts like below:

class CreatePosts < ActiveRecord::Migration
  def change
    create_table :posts do |t|
      t.string :title
      t.references :owner, polymorphic: true, index: true

      t.timestamps null: false
    end
    add_foreign_key :posts, :owners
  end
end

When we have done all these, our form for adding new form looks like this:

Rails generated textfield for association

We want to convert this text box into a grouped select box. Using Rails form helper grouped_collection_select, let’s go and modify the textfield into select box.

# posts/_form.html.erb
<%= f.grouped_collection_select :owner_id, [ User, Group ], :all, :model_name, :id, :name %>

Now it looks like this: (I added a few Users and Groups behind the scene)

Textfield change to grouped select box

Looks great. We have the interface we wanted but it has two issues.

  1. Post model is polymorphic, so we need :owner_type as well and not just :owner_id

  2. The generated <option> tags have values that doesn’t differentiate whether the option tag belongs to User or Group. So if I select, Group name with ID 1 in the select box, it’s same as selecting User name with ID 1. This is no good.

Values of option tags are not unique

Let’s solve #2 first before we tackle #1.

Instead using :id as the method to generate the value in

<%= f.grouped_collection_select :owner_id, [ User, Group ], :all, :model_name, :id, :name %>

use :to_global_id available in Rails 4.2.

<%= f.grouped_collection_select :owner_id, [ User, Group ], :all, :model_name, :to_global_id, :name %>

This brings us one step closer to solving our problem. Now the generated html should look like

Values of option tags are now unique

We still have to fix #1 i.e., storing :owner_type and we have one new problem :owner_id no more gets stored in the database. Since it’s an integer field and we are trying to pass on a global_id string the values are rejected. Both these issue can be solved by using a virtual attribute in the Post model.

class Post < ActiveRecord::Base
  belongs_to :owner, polymorphic: true

  def global_owner
    self.owner.to_global_id if self.owner.present?
  end

  def global_owner=(owner)
    self.owner = GlobalID::Locator.locate owner
  end

end

We have added a virtual attribute global_owner to our model with a setter and getter method. We now use this virtual attribute and this takes care of setting the right :owner_type and :owner_id.

Our updated form now looks like

<%= f.grouped_collection_select :global_owner, [ User, Group ], :all, :model_name, :to_global_id, :name %>

One last thing to do before we make this form completely work. Open up PostsController and allow the virtual attribute in the allowed params.

# PostsController
# Never trust parameters from the scary internet, only allow the white list through.
def post_params
  params.require(:post).permit(:title, :global_owner)
end

Our form is now completely working. It shows the options grouped by models ‘User’ and ‘Group’ and the selected values are stored and retrieved correctly.