Manual Polymorphic Creation in Rails
Finding documentation online to do this was pretty much a no go. The answer I’ve provided here was grasped from hints given on many websites of what could be done but without anything I could really sink my teeth into.
What this code allows you to accomplish is to create a belongs_to/has_many relationship to something while maintaining only one of each kind.
If something will, or may, have more than one belongs_to relationship, then you should use polymorphic relationships. The available online documentation on that pretty much just covers the basics. So I hope that this code outline will give you a deeper understanding and find success with polymorphic relationships with Rails from here on forward!
models/profile.rb
class Profile < ActiveRecord::Base has_many :socials, as: :sociable, dependent: :destroy accepts_nested_attributes_for :socials, allow_destroy: true end
models/social.rb
class Social < ActiveRecord::Base enum kind: [ :twitter, :google_plus, :facebook, :linked_in, :skype, :yahoo ] belongs_to :sociable, polymorphic: true validates_presence_of :kind validates_presence_of :username end
controllers/profiles_controller.rb
class ProfilesController < ApplicationController before_action :set_profile, only: [:show, :edit, :update, :destroy] before_action :set_social_list, only: [:new, :edit] def new @profile=Profile.new end def edit end private def set_profile @profile = Profile.find( params[:id] ) end def set_social_list @social_list = [ [ "LinkedIn ID", :linked_in ], [ "Facebook ID", :facebook ], [ "Twitter ID", :twitter ], [ "Google Plus ID", :google_plus ] ] end def profile_params params.require(:profile).permit(socials_attributes: [:id, :kind, :username, :_destroy]) end end
I’ve shortened the actual file for just what’s relevant here. You will need any other parameters permitted for your use case. The rest can remain untouched.
controllers/application_controller.rb
class ApplicationController < ActionController::Base def one_by_kind(obj, kind) obj.where(:kind => kind).first || obj.where(:kind => kind).build end helper_method :one_by_kind end
This is where the magic will happen. It’s designed after .where(…).first_or_create but uses build instead so we don’t have to declare build for the socials object in the profile_controller.
And lastly the all important view:
(polymorphics most undocumented aspect.)
views/profiles/_form.html
<% @social_list.each do |label, entry| %> <%= f.fields_for :socials, one_by_kind(@profile.socials, @profile.socials.kinds[entry]) do |a| %> <%= a.hidden_field :kind, {value: entry} %><%= label %>: <%= a.text_field :username %> <% end %> <% end %>
The @social_list is defined in the profile_controller and is an array of label & kind pairs. So as each one gets passed through, the one_by_kind method we defined in the application_controller seeks for the first polymorphic child that has the right kind which we’ve named entry. If the database record isn’t found, it is then built. one_by_kind then hands back the object for us to write/update.
This maintains one view for both creating and updating polymorphic children. So it allows for a one of each kind within your profile and social relation.
Please comment, share, subscribe to my RSS Feed,and follow me on twitter @6ftdan!
God Bless! – Daniel P. Clark
Johnny Chan
March 11, 2016 - 6:56 am
Hi Daniel, I have had a go working through this blog post tutorial and I am getting errors at the `_form.html.erb` – somewhere around the invocation of `one_by_kind()` helper method. I am sorry for the ask but I would be very grateful if you could help me out debugging this? I have uploaded my Rails 4 project to GitHub [here](https://github.com/Atlas7/params101). I have only started using Rails for 2 months and still a newbie. Thank you!!!
Daniel P. Clark
March 11, 2016 - 7:38 am
In your “params101/app/views/profiles/new.html.erb” file you need to change the code from
render "form"
to
render partial: "form"
Let me know how that works for you.
Daniel P. Clark
March 11, 2016 - 8:25 am
I found that the polymorphic model relationship wasn’t created in the DB.
Johnny Chan
March 11, 2016 - 9:16 am
Hi Daniel,
Thanks so much for the super debugging session via Twitter chat! (https://twitter.com/6ftdan/status/708282028517675008). Now I have managed to at last render the partial `_form.html.erb` from `new.htm.erb` ok.
My next step will be to play around with actually creating the object via that form (i.e. will probably need a submit button in that form, plus some fiddling with the ProfilesController.)
Have started playing around with the Rails console – as a Rails newbie hopefully I can start using your trick shortly!
Thanks!
Johnny