ruby-on-railsformsserializationfields-for

(Rails) fields_for a serialized column are not being populated with data


i won't to submit an @order via a form_with in Rails 5.2. The @order is an instance of the Order class which has a serialized column for the address fields. The address fields are filled by fields_for within the order form, when submitting the form to the OrdersController all the fields / values are being passed correctly.

The problem is: if @order fails validation, the OrdersController renders the form view again with @order's errors, but here the fields_for address are not being populated by the :address hash.

I saw quite some hacky solutions to convert a serialized column into attr_accessors. Is there a convenient solution to populate form fields from a serialized column in Rails?

Here my code …

order.rb

class Order < ApplicationRecord
  serialize :address

  …

  validate  :address_validator

  …

  private

    def address_validator
      required_fields = [:firstname, :lastname, :line1, :city, :postal_code, :country]
      required_fields.each do |field|
        self.errors.add(:base, "Address / #{field.to_s.titleize} can't be blank") if self.address[field.to_s].blank?
      end
    end

    …
end

new.html.erb

<%= form_with model: @order, id: 'order-form', class: 'form', local: true do |f| %>
  <%= render 'shared/form_errors', object: f.object %>

  …

  <%= f.fields_for :address do |g| %>
    <%= render 'orders/address_fields', f: g %>
  <% end %>

  …

<% end %>

_address_fields.html.erb

<div class='form__row columns columns--responsive-to-small columns--with-gutter'>
  <div class='form__input form__input--mandatory'>
    <%= f.label :firstname, 'Firstname' %>
    <%= f.text_field :firstname %>
  </div>

  <div class='form__input form__input--mandatory'>
    <%= f.label :lastname, 'Lastname' %>
    <%= f.text_field :lastname %>
  </div>
</div>

<div class='form__row'>
  <div class='form__input form__input--mandatory'>
    <%= f.label :line1, 'Address (line 1)' %>
    <%= f.text_field :line1 %>
  </div>
</div>

…

After submitting the form the @order object has the following values (address values are present)

(byebug) @order
#<Order id: nil, order_id: "HvMB00KS-73e1fc", …, created_at: nil, updated_at: nil, address: {"firstname"=>"Rocky", "lastname"=>"Marciano", "line1"=>"Saplestreet 123", "line2"=>"", "city"=>"Clashtown", "postal_code"=>"18726", "country"=>"Germany"}, email: "test@mail.com", products: … >

Thanks!


Solution

  • There is a convenient (not well documented) solution for this called store_accessor that can be used together with a serialized attributes hash in order to create accessors for its keys. I found this thanks to Using PostgreSQL and jsonb with Ruby on Rails by Nando Vieira.

    Using the exemplary address hash from above, we can define …

    class Order < ApplicationRecord
      serialize :address
      store_accessor :address, :firstname, :lastname, :postal_code, …
      …
    end
    

    to set and read the address' attributes like …

    order = Order.new
    order.firstame = 'Billy'
    order.firstname
    #=> "Billy"
    order.address['firstname']
    #=> "Billy"
    

    in a form for an @order the keys of address can be set directly and the form will be populated respectively …

    <%= form_with model: @order, local: true do |f| %>
    
      <%= f.label :firstname, 'Firstname' %>
      <%= f.text_field :firstname %>
    
      <%= f.label :lastname, 'Lastname' %>
      <%= f.text_field :lastname %>
    
    <% end %>
    

    using strong parameters in OrdersController like …

    params.require(:order).permit(:firstname, :lastname)