About the author:
Eduard Horiach is a Ruby on Rails Developer at Syndicode. Currently, he maintains a platform that provides water utilities and facility managers with the tools to prevent FOG (fat, oil, and grease) from entering sewer and drainage systems. Also, Eduard is passionate about building clean architecture in Rails Applications.
In this article, you can find information on developing custom fields feature for ActiveRecord objects using Postgres JSON fields and decorator patterns.
What are the custom fields?
Sometimes when we’re developing an application, we run into situations when we don’t know precisely what types of fields we need for a model or requirements. In this case, you can use custom fields.
One example might be a model for products. A product usually has fixed fields like name, description, price, and so on. But different kinds of products may have different types of fields like location to keep track of the product’s site. Or maybe you just want to add an additional_price.
The list of potential fields that you can associate with a product is endless, and creating a column for all of them in a product table will obviously not work.
These requirements can be solved using a JSON data type in PostgreSQL, which allows you to store key/value structures, just like a dictionary or hash.
What is ActiveRecord object?
Active Record in Ruby on Rails Architecture is the M in MVC – the model is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database.
Let’s Begin
Before starting, you should have bootstrapped the Rails app with Postgres as a database. I suppose you’re familiar with Ruby, Ruby on Rails, HTML, SCSS, Javascript, and Git to the start work with this tutorial. If you can’t wait to look at the ready Rails application with custom fields, check it out here.
Our goal is to create a Rails application where users can create their own shop with products that will have custom fields.
Create a shops table
Let’s start from the first step by scaffolding resources for the Shop model.
rails generate scaffold Shop name:string
*Rails scaffolding is a quick way to generate some of the major pieces of an application. If you want to create the models, views, and controllers for a new resource in a single operation, scaffolding is the tool for the job.
Change migration file for shops table in db/migrate folder to this.
class CreateShops < ActiveRecord::Migration[5.1]
def change
create_table :shops do |t|
t.string :name, null: false
t.timestamps
end
end
end
Add validation for the name in the Shop model.
# app/models/shop.rb
class Shop < ApplicationRecord
validates :name, presence: true
end
Next step is a Creating a Products Table
We should create a table for products that will be extended by custom fields and belongs to the Shop.
Let’s make scaffold for products table.
rails generate scaffold Product name:string description:text price:decimal
Change code inside migration file to this.
class CreateProducts < ActiveRecord::Migration[5.1]
def change
create_table :products do |t|
t.string :name
t.text :description
t.decimal :price
t.belongs_to :shop, null: false, foreign_key: { delete: :cascade }
t.jsonb :custom_fields, null: false, default: {}, index: { using: :gin }
t.timestamps
end
end
end
Let’s take a closer look in this two lines.
t.jsonb :custom_fields, null: false, default: {}, index: { using: :gin } and t.belongs_to :shop, null: false, foreign_key: { delete: :cascade }
In the field, with the name custom_fields, we will store values of filled custom fields for a record.
Changed product model should looks like this.
#app/models/product.rb
class Product < ApplicationRecord
belongs_to :shop, dependent: :delete_all
validates :name, :price, presence: true
end
Creating a table for a custom fields
Move on from the first steps of creating resources for shop and product models. Let’s create a table where we should be storing information about custom fields in our Rails application.
rails scaffold CustomField label:string internal_name:string
Change a file with migration should look like that.
class CreateCustomFields < ActiveRecord::Migration[5.1]
def change
create_table :custom_fields do |t|
t.belongs_to :shop, null: false, foreign_key: { delete: :cascade }
t.string :label, null: false
t.string :internal_name, null: false
t.string :field_type, default: 0, limit: 2, null: false
t.text :description
t.timestamps
end
add_index :custom_fields, %i[internal_name], unique: true
end
end
The next step add validation and associations to newly generated the CustomField class.
#app/models/custom_field.rb
class CustomField < ApplicationRecord
belongs_to :shop, dependent: :delete_all
validates :label, :internal_name, :type, presence: true
before_validation :parameterize_internal_name
enum field_type: {
text: 0,
number: 1
}
def parameterize_internal_name
return if internal_name.blank?
self.internal_name = internal_name.parameterize(separator: '_').underscore
end
end
Add association has_many :custom_properties for Product Model.
class Product < ApplicationRecord
belongs_to :shop
validates :name, :price, presence: true
has_many :custom_properties
end
Extend product model using decorator pattern
The decorator pattern allows us to add behavior to an instance without affecting the behavior of all the instances of that class. We can “decorate” or add behavior to, a model instance before passing it to a view.
In this tutorial, we use SimpleDelegator Class that allows us to override delegated methods with super, calling the corresponding method on the wrapped object.
Let’s create Decorator for CustomField class that will display properties.
class CustomFieldDecorator < SimpleDelegator
def initialize(object)
super
init_custom_accessors
end
protected
def init_custom_accessors
fields = shop.custom_fields.pluck(:internal_name)
return if fields.empty?
__getobj__.singleton_class.class_eval do
store_accessor :custom_fields, *fields
end
end
end
Method store_accessor generates the accessor methods for Active Record Objects. Be aware that these columns use a string keyed hash and do not allow access using a symbol.
Now, we have created the basic architecture for custom field feature for Rails Application.
Let’s move on and use them in the controllers and views part of our Rails Application.
In this article, we move on from the first part of the tutorial and create controllers and views for our shop with products that has custom fields, and at the end of the article, we will test our Rails Application.
Specifying Nested Routes for the Shop Model
Open your config/routes.rb file to modify the relationship between your resourceful routes.
Currently, the file looks like this:
Rails.application.routes.draw do
resources :shops
resources :custom_fields
resources :products
end
The current code establishes an independent relationship between our routes when what we would like to express is a dependent relationship between shops and their associated products and custom fields.
Rails.application.routes.draw do
resources :shops do
resources :custom_fields
resources :products
end
end
Save and close the file when you are finished editing.
With these changes in place, you can move on to updating your products controller.
Updating the Products Controller
The association between our models gives us methods that we can use to create new product instances associated with particular shops. To use these methods, we will need to add them to our products controller.
Open the products controller file:
nano app/controllers/products_controller.rb
Change code of products controller to this:
class ProductsController < ApplicationController
before_action :set_shop
before_action :set_product, only: [:show, :edit, :update, :destroy]
before_action :decorate_product, only: [:show, :edit, :update]
def index
@products = @shop.products
end
def show; end
def new
@product = CustomFieldDecorator.new(@shop.products.build)
end
def edit; end
def create
product = CustomFieldDecorator.new(@shop.products.new)
product.assign_attributes(product_params)
respond_to do |format|
if product.save
format.html { redirect_to shop_products_path(@shop), notice: 'Product was successfully created.' }
format.json { render :show, status: :created, location: product }
else
format.html { render :new }
format.json { render json: product.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @product.update(product_params)
format.html { redirect_to shop_products_path(@shop), notice: 'Product was successfully updated.' }
format.json { render :show, status: :ok, location: @product }
else
format.html { render :edit }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end
def destroy
@product.destroy
respond_to do |format|
format.html { redirect_to shop_products_url(@shop), notice: 'Product was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def set_shop
@shop = Shop.find(params[:shop_id])
end
def set_product
@product = @shop.products.find(params[:id])
end
def decorate_product
@product = CustomFieldDecorator.new(@product)
end
def product_params
params.require(:product).permit(:name, :description, :price, *custom_fields)
end
def custom_fields
@shop.custom_fields.pluck(:internal_name)
end
end
Let’s look closer to new methods that we added to the controller.
def decorate_product
@product = CustomFieldDecorator.new(@product)
end
Method decorate_product decorates a product object that’s associated with the specific shop instance and gives us the capability to use custom fields as standard properties of product object.
def product_params
params.require(:product).permit(:name, :description, :price, *custom_fields)
end
def custom_fields
@shop.custom_fields.pluck(:internal_name)
end
Another change that we added is allowing custom fields in strong params.
This means that we can set our own custom fields through form submissions.
Updating the Custom Fields Controller
Our CustomField Model also depends on the Shop model through association. So we should make a Custom Fields Controller nested from the Shop Model.
Open the products controller file:
nano app/controllers/custom_fields_controller.rb
Change code of custom fields controller to this:
class CustomFieldsController < ApplicationController
before_action :set_shop
before_action :set_custom_field, only: [:show, :edit, :update, :destroy]
def index
@custom_fields = @shop.custom_fields
end
def show
end
def new
@custom_field = @shop.custom_fields.build
end
def edit
end
def create
@custom_field = @shop.custom_fields.new(custom_field_params)
respond_to do |format|
if @custom_field.save
format.html { redirect_to shop_custom_fields_path(@shop), notice: 'Custom field was successfully created.' }
format.json { render :show, status: :created, location: @custom_field }
else
format.html { render :new }
format.json { render json: @custom_field.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @custom_field.update(custom_field_params)
format.html { redirect_to shop_custom_fields_path(@shop), notice: 'Custom field was successfully updated.' }
format.json { render :show, status: :ok, location: @custom_field }
else
format.html { render :edit }
format.json { render json: @custom_field.errors, status: :unprocessable_entity }
end
end
end
def destroy
@custom_field.destroy
respond_to do |format|
format.html { redirect_to shop_custom_fields_url(@shop), notice: 'Custom field was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def set_shop
@shop = Shop.find(params[:shop_id])
end
def set_custom_field
@custom_field = @shop.custom_fields.find(params[:id])
end
def custom_field_params
params.require(:custom_field).permit(:label, :internal_name, :field_type, :description)
end
end
Now our products and custom fields are associated with particular shops. We can modify the view templates themselves, which are where users will pass in and modify post information about specific resources.
Modifying Views
Our view template revisions will involve changing the templates that relate to products. Let’s start with the foundational template for our products – the form partial reused across edit and new templates. Open that form now:
nano app/views/products/_form.html.erb
First that we should do is сhange the first line of the file to look like this, reflecting the relationship between our shop and products resources:
<%= form_with(model: [@shop, product], local: true) do |form| %>
Next step, add the section for custom fields that relate to the shop.
<div class="field">
<% @shop.custom_fields.each do |field| %>
<%= form.label field.internal_name %>
<%= custom_field_input(form, field) %>
<% end %>
</div>
As you can see, we use the custom_field_input method for displaying inputs fields of our custom fields. Method custom_field_input should be placed in app/helpers/custom_fields_helper.rb you can copy the code of the practice from our GitHub repository.
Result after pasting editions.
<%= form_with(model: [@shop, product], local: true) do |form| %>
<% if product.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:</h2>
<ul>
<% product.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name, id: :product_name %>
</div>
<div class="field">
<%= form.label :description %>
<%= form.text_area :description, id: :product_description %>
</div>
<div class="field">
<%= form.label :price %>
<%= form.text_field :price, id: :product_price %>
</div>
<div class="field">
<% @shop.custom_fields.each do |field| %>
<%= form.label field.internal_name %>
<%= custom_field_input(form, field) %>
<% end %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
Next, open and the show template:
nano app/views/posts/show.html.erb
Make the following edits in the file:
<p id="notice"><%= notice %></p>
<p>
<strong>Name:</strong>
<%= @product.name %>
</p>
<p>
<strong>Description:</strong>
<%= @product.description %>
</p>
<p>
<strong>Price:</strong>
<%= @product.price %>
</p>
<p>
<% @shop.custom_fields.each do |field| %>
<strong> <%= field.label %>:</strong>
<%= @product.custom_fields[field.internal_name] %>
<% end %>
</p>
<%= link_to 'Edit', edit_shop_product_path(@shop, @product) %> |
<%= link_to 'Back', shop_products_path %>
This code section below in our show template displays the values of our custom fields.
<% @shop.custom_fields.each do |field| %>
<strong> <%= field.label %>:</strong>
<%= @product.custom_fields[field.internal_name] %>
<% end %>
Now you are made changes to your controllers, and views to ensure that products are always associated with a particular shops. As a final step, let’s start testing our Rails Application.
Testing our Application
Start your local server by running this shell script
rails s
Let’s create our first shop record in our Rails application. Visit localhost:3000/shops/new and enter the name for a shop after a click on the “Create Shop” Button. See picture 1.1.

We need to store information about our custom fields, so create our first custom field record associated with the product model. Use should visit /shops/1/custom_fields/new and enter data that displayed in picture 1.2.

Visit /shops/1/products/new path. As you can notice, now our products form a custom field that can be set as a regular field. You can fill product form as displayed in picture 1.3.

In picture 1.4 we see that our additional custom field feature works, and we can create other custom fields that we need.

Summary
That’s it. We’ve archive our goal, we have created a Rails application where users can create their own shop with products that will have custom fields.
In the first part, I told you about the Decorator Pattern and ActiveRecord objects, and we discover how to create the basic architecture for custom fields feature.
And in the second part, we are the finish developing the controllers and views layer of our Rails Application.
Thank you for going through this tutorial with me, hope it was helpful and you like it!
You can find Syndicode on GitHub and explore the example of the readymade app.