How to edit a Rails serialized field in a form?

Ruby on-RailsFormsSerialization

Ruby on-Rails Problem Overview


I have a data model in my Rails project that has a serialized field:

class Widget < ActiveRecord::Base
  serialize :options
end

The options field can have variable data info. For example, here is the options field for one record from the fixtures file:

  options:
    query_id: 2 
    axis_y: 'percent'
    axis_x: 'text'
    units: '%'
    css_class: 'occupancy'
    dom_hook: '#average-occupancy-by-day'
    table_scale: 1

My question is what is the proper way to let a user edit this info in a standard form view?

If you just use a simple text area field for the options field, you would just get a yaml dump representation and that data would just be sent back as a string.

What is the best/proper way to edit a serialized hash field like this in Rails?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

If you know what the option keys are going to be in advance, you can declare special getters and setters for them like so:

class Widget < ActiveRecord::Base
  serialize :options

  def self.serialized_attr_accessor(*args)
    args.each do |method_name|
      eval "
        def #{method_name}
          (self.options || {})[:#{method_name}]
        end
        def #{method_name}=(value)
          self.options ||= {}
          self.options[:#{method_name}] = value
        end
        attr_accessible :#{method_name}
      "
    end
  end

  serialized_attr_accessor :query_id, :axis_y, :axis_x, :units
end

The nice thing about this is that it exposes the components of the options array as attributes, which allows you to use the Rails form helpers like so:

#haml
- form_for @widget do |f|
  = f.text_field :axis_y
  = f.text_field :axis_x
  = f.text_field :unit

Solution 2 - Ruby on-Rails

Well, I had the same problem, and tried not to over-engineer it. The problem is, that although you can pass the serialized hash to fields_for, the fields for function will think, it is an option hash (and not your object), and set the form object to nil. This means, that although you can edit the values, they will not appear after editing. It might be a bug or unexpected behavior of rails and maybe fixed in the future.

However, for now, it is quite easy to get it working (though it took me the whole morning to figure out).

You can leave you model as is and in the view you need to give fields for the object as an open struct. That will properly set the record object (so f2.object will return your options) and secondly it lets the text_field builder access the value from your object/params.

Since I included " || {}", it will work with new/create forms, too.

= form_for @widget do |f|
  = f.fields_for :options, OpenStruct.new(f.object.options || {}) do |f2|
    = f2.text_field :axis_y
    = f2.text_field :axis_x
    = f2.text_field :unit

Have a great day

Solution 3 - Ruby on-Rails

emh is almost there. I would think that Rails would return the values to the form fields but it does not. So you can just put it in there manually in the ":value =>" parameter for each field. It doesn't look slick, but it works.

Here it is from top to bottom:

class Widget < ActiveRecord::Base
    serialize :options, Hash
end

<%= form_for :widget, @widget, :url => {:action => "update"}, :html => {:method => :put} do |f| %>
<%= f.error_messages %>
    <%= f.fields_for :options do |o| %>
        <%= o.text_field :axis_x, :size => 10, :value => @widget.options["axis_x"] %>
        <%= o.text_field :axis_y, :size => 10, :value => @widget.options["axis_y"] %>
    <% end %>
<% end %>

Any field you add in the "fields_for" will show up in the serialized hash. You can add or remove fields at will. They will be passed as attributes to the "options" hash and stored as YAML.

Solution 4 - Ruby on-Rails

I've been struggling with a very similar problem. The solutions I found here were very helpful to me. Thank you @austinfromboston, @Christian-Butske, @sbzoom, and everyone else. However, I think these answers might be slightly out-of-date. Here's what worked for me with Rails 5 and ruby 2.3:

In the form:

<%= f.label :options %>
<%= f.fields_for :options do |o| %>
  <%= o.label :axis_y %>
  <%= o.text_field :axis_y %>
  <%= o.label :axis_x %>
  <%= o.text_field :axis_x %>
  ...
<% end %>

and then in the controller I had to update the strong parameters like so:

def widget_params
    params.require(:widget).permit(:any, :regular, :parameters, :options => [:axis_y, :axis_x, ...])
end

It seems to be important that the serialized hash parameter comes at the end of the list of parameters. Otherwise, Rails will expect the next parameter to also be a serialized hash.

In the view I used some simple if/then logic to only display the hash if it is not empty and then to only display key/value pairs where the value was not nil.

Solution 5 - Ruby on-Rails

I was facing the same issue, after some research i found a solution using Rails' store_accessor to make keys of a serialized column accessible as attributes.

With this we can access "nested" attributes of a serialized column …

# post.rb
class Post < ApplicationRecord
  serialize :options
  store_accessor :options, :value1, :value2, :value3
end

# set / get values
post = Post.new
post.value1 = "foo"
post.value1
#=> "foo"
post.options['value1']
#=> "foo"

# strong parameters in posts_controller.rb
params.require(:post).permit(:value1, :value2, :value3)

# form.html.erb
<%= form_with model: @post, local: true do |f| %>
  <%= f.label :value1 %>
  <%= f.text_field :value1 %>
  # …
<% end %>

Solution 6 - Ruby on-Rails

No need setter/getters, I just defined in the model:

serialize :content_hash, Hash

Then in the view, I do (with simple_form, but similar with vanilla Rails):

  = f.simple_fields_for :content_hash do |chf|
    - @model_instance.content_hash.each_pair do |k,v|
      =chf.input k.to_sym, :as => :string, :input_html => {:value => v}

My last issue is how to let the user add a new key/value pair.

Solution 7 - Ruby on-Rails

I will suggest something simple, because all the time, when user will save form You will get string. So You can use for example before filter and parse those data like that:

before_save do
  widget.options = YAML.parse(widget.options).to_ruby
end 

of course You should add validation if this is correct YAML. But it should works.

Solution 8 - Ruby on-Rails

I'm trying to do something similar and I found this sort of works:

<%= form_for @search do |f| %>
	<%= f.fields_for :params, @search.params do |p| %>
		<%= p.select "property_id", [[ "All", 0 ]] + PropertyType.all.collect { |pt| [ pt.value, pt.id ] } %>

		<%= p.text_field :min_square_footage, :size => 10, :placeholder => "Min" %>
		<%= p.text_field :max_square_footage, :size => 10, :placeholder => "Max" %>
	<% end %>
<% end %>

except that the form fields aren't populated when the form is rendered. when the form is submitted the values come through just fine and i can do:

@search = Search.new(params[:search])

so its "half" working...

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestioncpjolicoeurView Question on Stackoverflow
Solution 1 - Ruby on-RailsaustinfrombostonView Answer on Stackoverflow
Solution 2 - Ruby on-RailsChristian ButzkeView Answer on Stackoverflow
Solution 3 - Ruby on-RailssbzoomView Answer on Stackoverflow
Solution 4 - Ruby on-RailsJDenman6View Answer on Stackoverflow
Solution 5 - Ruby on-RailsR4ttlesnakeView Answer on Stackoverflow
Solution 6 - Ruby on-RailsgamovView Answer on Stackoverflow
Solution 7 - Ruby on-RailsmtfkView Answer on Stackoverflow
Solution 8 - Ruby on-RailsemhView Answer on Stackoverflow