Nested inputs for key value hash attributes

Kuba Krzempek edited this page Mar 30, 2018 · 6 revisions

This details how to create for a inputs for a Postgres JSON or JSONB column. JSON and JSONB support was added in Rails 4.2+.

Lets say you have a Post model with a JSON or JSONB column custom_fields. Rails accepts attributes for JSON columns as a hash:

{
  post: {
    title: "Nested inputs for key/value hash attributes."
    custom_fields: {
      hash_tags: "#rails,#simple_form,#postgres"
    }
  }
}

If this was a nested model we could have added out inputs by:

<%= simple_form_for(@post) do |f|
  f.input :title
  f.simple_fields_for(@post.custom_fields) do |cf|
    cf :hash_tags
  end
end %>

But this fails since our lowly hash does not respond to model_name which is part of the ActiveModel api. cf :hash_tags will also not work since our hash does not respond to hash_tags.

The solution is to dig out the old proxy or delegator pattern in the form of a decorator:

class CustomFieldsDecorator
  # @see http://api.rubyonrails.org/classes/ActiveModel/Naming.html
  MODEL_NAME = ActiveModel::Name.new(self.class, nil, 'custom_fields')

  def model_name
    MODEL_NAME
  end

  def initialize(hash)
    @object = hash.symbolize_keys
  end

  # Delegates to the wrapped object
  def method_missing(method, *args, &block)
    if @object.key? method
       @object[method]
    elsif @object.respond_to? method
       @object.send(method, *args, &block)
    end
  end

  def has_attribute? attr
    @object.key? attr
  end
end

Since our Decorator delegates to the hash we can call enumerable methods.

<% custom_fields = CustomFieldsDecorator.new(@post.custom_fields) %>
<%= simple_form_for(@post) do |f|
  f.input :title
  f.simple_fields_for(custom_fields) do |field|
    custom_fields.each do |key, value|
      field.input key
    end
  end
end %>

Solution 2: This solution, as proposed, works when you have a limited and known JSON objects. Create a Serializer like this:

# app/serializers/hash_serializer.rb
class HashSerializer
  def self.dump(hash)
    hash.to_json
  end

  def self.load(hash)
    (hash || {}).with_indifferent_access
  end
end

In order to access JSON object as symbols and add the following to the model:

class Post < ActiveRecord::Base
  serialize :preferences, HashSerializer
  store_accessor :preferences, :blog, :github, :twitter
end

It is now possible to directly access preferences objects like blog or twitter: post.blog, post.twitter, etc.

Solution 3:

It is something that sits between #1 and #2. Basically, it uses OpenStruct in the view and works well with known keys.

<%= simple_form_for(@post) do |f|
  f.input :title
  f.simple_fields_for :custom_fields, OpenStruct.new(@post.custom_fields) do |field|
    field.input :twitter
    field.input :github
  end
end %>
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.