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:
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)
Looks great. We have the interface we wanted but it has two issues.
Post model is polymorphic, so we need :owner_type as well and not just :owner_id
<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.
Let’s solve #2 first before we tackle #1.
Instead using :id as the method to generate the
<%= 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
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.