ruby-on-railsformscontrollersimple-form

How to provide a form field for a grandparent in Ruby-on-Rails SimpleForm?


I have 3 models in Rails, Category (grandparent) has_many Domain (parents), which has_many Url (children). Here, users can create/edit Url but not the other two; Domain model is automatically created/modified, according to Url, and is automatically associated to Url.

In the new/edit SimpleForm (simple_form) screen for Url, I provide a form field for Category so that the user can select/edit the Category the Url belongs to through Domain; namely, the user can actually update the content of the parent Domain#category_id (under the hood). Since Category is not the direct parent of Url, a simple f.association does not work out of the box.

So I write the model Url as follows

class Url < ApplicationRecord
  belongs_to :domain
  has_one :category, through: :domain

  def category_id
    category ? category.id : nil
  end
  attr_writer :category_id

and the view _form.html.erb

    <%= f.input :category_id, collection: Category.all %>

Although the form looks OK on the browser, showing the currently associated Category of Url as expected, and although params returns the expected Category-ID value and I can set it to @url instance, the Url controller still fails to update the assocition (of parent Domain) to Category. How can I make it work?


Solution

  • accepts_nested_attributes_for handles this:

    class Category < ApplicationRecord
    end
    
    class Domain < ApplicationRecord
      belongs_to :category
    end
    
    class Url < ApplicationRecord
      belongs_to :domain
    
      accepts_nested_attributes_for :domain
    end
    
    <% @url.build_domain unless @url.domain %>
    
    <%= simple_form_for @url do |f| %>
      <%= f.simple_fields_for :domain do |ff| %>
        <%= ff.association :category, label_method: :name %>
      <% end %>
      <%= f.submit %>
    <% end %>
    

    Permit nested fields in controller:

    params.expect(url: [domain_attributes: [:category_id, :id]])
    

    If you don't want nested fields, you can delegate to domain:

    class Category < ApplicationRecord
    end
    
    class Domain < ApplicationRecord
      belongs_to :category
    end
    
    class Url < ApplicationRecord
      belongs_to :domain, autosave: true # autosave - to save domain on url save
    
      delegate :category_id, :category_id=, to: :domain
    
      def domain
        super || build_domain # always have domain to delegate to
      end
    end
    
    <%= simple_form_for @url do |f| %>
      <%= f.input :category_id, collection: Category.all, label_method: :name %>
      <%= f.submit %>
    <% end %>
    

    Permit fields in controller:

    params.expect(url: [:category_id])