Table of Contents
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!