Syndicode
Contact Us
Contact Us
4.9 on Clutch
Delivery Team, Marketing Team, Oleksandr Subbotin

Manage custom fields for an ActiveRecord object in Rails

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.

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.

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.

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.

Picture 1.4

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.