Nested form fields implementation in Active Model

Introduction

In the ActiveRecord model, ‘has_many’ and ‘accepts_nested_attributes_for’ are default components to build form objects so as to create multiple records.

 

ActiveModel::Model is an excellent way to make objects behave like ActiveRecord. But the ActiveModel lacks one key feature, which is the ‘accepts_nested_attributes_for’. As we know that this is a primary attribute for nested fields, in this article, we will see how to implement nested form fields in ActiveModel, i.e without ActiveRecord.

Why we use ActiveModel?

If we want to make use of form data that doesn’t necessarily persist to an object or used for other non-active record purposes like API data set, Active Model Helper Modules come into role. They reduce a lot of  complexity.

 

For example, the Data layer(API – data node) is running in a separate node(API node) and its built in ActiveRecord/Sequel ORM and UI is running in a separate node. This uses the ActiveModel, because the data does not persist in the UI layer. Here we are using ActiveModel to make the UI controller clean and to build a form object in an easier way.

 

In the UI layer, when we try to build form objects to create multiple records (Eg: An organization having multiple recruitment steps), ActiveModel doesn’t have ‘accepts_nested_attributes_for’ feature to implement this.

To resolve this problem, initially we looked out for gems, but after a while, we found the perfect solution.

 

Step 1: Defining form Objects in Model

 

If an object with a one-to-many association is instantiated with a params hash, and that hash has a key for the association, Rails will call the <association_name>_attributes= method on that object. So, we need to define our form object in Model.

 


class Organization
include ActiveModel::Model
attr_accessor :id, :name, :logo, :organization_family, :recruitments

def recruitments_attributes=(attributes)
@recruitments ||= [] attributes.each do |i, recruitment_params|
@recruitments.push(Recruitment.new(recruitment_params))
end
end

end
class Recruitment
include ActiveModel::Model
attr_accessor :id, :title, :time_duration, :duration, :description, :organisation_id
end

 

Step 2: Handling in controller

 

In controller action, we are trying to build organization recruitment object based on two conditions.

● Condition 1: Organization already has some recruitments, and so the object built is as below:

 

 


rec = [] @organization.recruitments.map do |r|
rec << Recruitment.new(r)
end
@organization.recruitments = rec

 

● Condition 2: Object doesn’t have any recruitments yet, and so the object built is as below:

 


@organization.recruitments = [] @organization.recruitments << Recruitment.new(organisation_id: @organization

 

Put together, the whole method looks like below:


def descriptions
# API connection as service object
client = Services::Client.new
response = client.get("/organizations/#{params[:id]}/edit")
@data = response["value"]["select_data"] # After getting data from API layer
if response['success'] @organization = Organization.new(response["value"]["organization"])
if @organization.recruitments.present?
rec = [] @organization.recruitments.map do |r|
rec << Recruitment.new(r)
end
@organization.recruitments = rec
else
@organization.recruitments = [] @organization.recruitments << Recruitment.new(organisation_id: @organization.id)
end
else
redirect_to organizations_path
end
end

Step 3: Handling in view

 

Now we need a form for this model. If Object doesn’t have any recruitments yet (condition 2), we need to make sure that the form has at least one ‘fields_for’ block to render, by giving it one on initialization.


<div class=’form’>
<%= form_for @organization, :url => "/organizations/#{params[:id]}", method: 'put' do |f| %>
<%= f.text_field :name %>
<%= f.text_field :organization_family %>
<%= f.fields_for :recruitments do |c| %>
<%= c.text_field :title %>
<%= c.select :time_duration, 1..40,{:prompt=> "Select"} %>
<%= c.radio_button :duration, true %>
<%= c.radio_button :duration, false %>
<%= c.text_area :description %>
<% end %>
<%= f.submit "Submit" %>
<% end %>
</div>

 

 

Conclusion

 

This is how we implement nested form fields using ActiveModel and without Active Record. This produces the desired results to us using ActiveModel, just as we would have obtained if we used ActiveRecord, using ‘accepts_nested_attributes_for’… And we are good to go!