Search code examples
ruby-on-railsrails-activerecordpolymorphic-associations

Instantiating multiple instances of polymorphic child manually


I have multiple social networks available in my model:

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

I want to declare manually the kinds used. Maybe I need to have an alternative to fields_for?

<%= f.fields_for :socials do |a| %>
  <%= a.hidden_field :kind, {value: :facebook} %> Facebook ID: <%= a.text_field :username, placeholder: "kind" %>
  <%= a.hidden_field :kind, {value: :twitter} %> Twitter ID: <%= a.text_field :username, placeholder: "kind" %>
  <%= a.hidden_field :kind, {value: :google_plus} %> Google ID: <%= a.text_field :username, placeholder: "kind" %>
  <%= a.hidden_field :kind, {value: :linked_in} %> Linked In ID: <%= a.text_field :username, placeholder: "kind" %>
<% end %>

But I get just one value saved and displayed for all four IDs.

enter image description here

When doing a fields_for on each individual item I get repeated kinds and repeated values

NOTE: There should be only one of each kind associated with this profile form.


I believe that I need to use something like find_or_create_by to ensure only one of each kind is made and loaded in the editor as the fields_for simply loads everything in the order they were saved. Maybe showing how this Rails find_or_create by more than one attribute? could be used with just kind.

I need to ensure that product will only save one of each kind and when you edit it; it will load correctly by kind and not just any belonging to.

Since in my example all four will display what was saved in the first field on the edit page it's clear it's not ensuring the kind at the moment.


I'd like to use something like this in my application_controller.rb

def one_by_kind(obj, kind)
  obj.where(:kind => kind).first_or_create
end

How would I substitute the fields_for method with this?


Solution

  • Manual Polymorphic Creation in Rails

    Alright I've discovered the solution. Here's what I've got.

    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.com/pub/", :linked_in],
          ["facebook.com/", :facebook],
          ["twitter.com/", :twitter],
          ["google.com/", :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 creation and updating polymorphic children. So it allows for a one of each kind within your profile and social relation.