Cocoon Gem in Ruby on Rails 7
In this article, we are going to learn how to create a complex form or nested form, and add a dynamic field for any field in our application using the cocoon gem in Rails 7. Complex or Nested forms which mean we will handle nested models and attributes in one single form.
So, let’s get begin
→ Rails ~> 7.0.4 and Ruby ~> 3.1.0 are utilized for this application.
Points to be discussed
- What is Cocoon Gem?
- Implementation of the cocoon in rails 7
- Preview
- References
1) What is Cocoon Gem?
As we discussed Essentially, this gem is used for nested forms, multiple models, and many field attributes in a single form. For example, we have an address field, so any person can have multiple addresses; hence, storing that data and rendering that data in the cocoon gem is useful.
Cocoon makes it easier to handle nested forms. — Cocoon Github
2) Implementation of the cocoon in rails 7
We are going to add the cocoon gem to our employee CRUD application.
Recently, we developed Employee CRUD in Rails 7 with Bootstrap styling.
Thus, using the cocoon gem, we will add the “Address Field” to that CRUD and make it dynamic.
Here is a reference to my previously published blog for creating CRUD in Rails 7.
So let’s begin by adding the cocoon gem to the gemfile. With the cocoon gem, we also need to add the jquery-rails gem as well, so just add these two gems in your Gemfile
gem 'jquery-rails'
gem 'cocoon'
Then, Run bundle install
for installing these gems.
Now add jquery.js to assets.rb file for precompiling.
Rails.application.config.assets.precompile += %w( jquery.js )
After doing this just Remember to run rails assets:precompile
Let's include jquery in the application.html.erb
<%= javascript_include_tag "jquery" %>
→ *When the main page is loaded, this line includes jQuery first in our application.
This entire setup is for jQuery only.
Let’s use importmap (a new feature in Rails 7) to add the cocoon gem:
bin/importmap pin @nathanvda/cocoon
→ “@nathanvda/cocoon” will be pinned in the importmap.rb file.
Finally, for the cocoon gem, instead of importing cocoon, you need to import “@nathanvda/cocoon” in application.js.
Here is the syntax for that.
import "@nathanvda/cocoon"
Now that we’ve finished with Cocoon and jQuery, let’s add the code for dynamic nested fields.
As I mentioned about employee CRUD, in our CRUD we have these fields, as you can see in the below snapshot.
As we want to add an address field, we have to generate an address model with house_number
, society_name
, area
, city
fields. So for that, fire this command.
rails g model Address house_number:integer society_name area city
This will create and invoke some files inside that we need to work with the model file and one migration file only. And in the migration file, there’s some code for creating an Address table with the given fields.
migration file:
class CreateAddresses < ActiveRecord::Migration[7.0]
def change
create_table :addresses do |t|
t.integer :house_number
t.string :society_name
t.string :area
t.string :city
t.timestamps
end
end
end
Now we need to add a reference to the employee in this file, so just edit this file and add this line: t.references :employees
, and it looks like
class CreateAddresses < ActiveRecord::Migration[7.0]
def change
create_table :addresses do |t|
t.integer :house_number
t.string :society_name
t.string :area
t.string :city
t.references :employee
t.timestamps
end
end
end
If you want to pass null :false
with a reference, you can do that also, but here we are keeping it simple.
Now run rails db:migrate
it will make some changes to our schema. adding a new addresses table with the given fields and also adding employee_id as a reference in it.
Apart from it, suppose you don’t want to make any changes in the migration file for giving references and want to add references using another migration, so you need to follow these steps:
Run this code
rails g migration AddEmployeeToAddress employee:references
It will generate one migration with the following code:
class AddEmployeeToAddress < ActiveRecord::Migration[7.0]
def change
add_reference :addresses, :employee, null: false, foreign_key: true
end
end
and migrate it using rails db:migrate
The work of migrating files has been completed; now let’s move on to the model file.
open app/models/employee.rb and paste the below code.
class Employee < ApplicationRecord
has_many :addresses
accepts_nested_attributes_for :addresses,
allow_destroy: true,
reject_if: proc { |att| att['house_number'].blank? || ['society_name'].blank? || ['city'].blank? || ['area'].blank? }
end
open app/models/address.rb and paste the below code.
class Address < ApplicationRecord
belongs_to :employee
end
That’s it for model. Now it is the turn of the controller file.
Open employees_controller.rb and add parameters for the address_attributes field.
addresses_attributes:[:id, :house_number, :society_name, :area, :city, :_destroy]
→ Note :we are also passing the id here to ensure that the fields are not duplicated while editing the data.
Previous params :
params.require(:employee).permit(:employee_name, :gender, { hobbies: [] })
New params :
params.require(:employee).permit(:employee_name, :gender, { hobbies: [] }, addresses_attributes: [:id, :house_number, :society_name, :area, :city, :_destroy])
After that, you’ll need to add partials and some code for rendering dynamic fields.
Add the following code into the new.html.erb file:
<div class="addresses">
<%= f.fields_for :addresses do |address| %>
<%= render 'address_fields', f: address %>
<% end %>
</div>
<%= link_to_add_association "add address", f, :addresses, data: {association_insertion_node: '.addresses', association_insertion_method: :append} %>
So now we have our entire new.html.erb and it contains
<div class="set-center flex-column">
<div class="card shadow" style="width: 36rem;">
<div class="card-header">
<h2 class="text-center">Employee Form</h2>
</div>
<div class="card-body">
<!--Error Message-->
<% if flash[:errors] %>
<% flash[:errors].each do |error| %>
<p class="text-danger"><%= error %></p>
<% end %>
<% end %>
<!-- Form Started Here -->
<%= form_with model: @employee do |f| %>
<%= f.label :employee_name, "Employee Name :", class:"mt-3" %>
<%= f.text_field :employee_name ,placeholder: "Enter Employee's Name",class:"mb-2 form-control" %>
<br>
<!-- Radio Button For Gender -->
<div class="form-group">
<%= f.label "Gender :" %>
<%= f.radio_button :gender, "male" %>
<%= f.label :gender, "Male" %>
<%= f.radio_button :gender, "female" %>
<%= f.label :gender, "Female" %>
</div>
<br>
<!-- Checkbox For Hobbies -->
<div class="form-group">
<%= f.label "Hobbies :" %>
<%= f.check_box :hobbies, { multiple: true },"Reading", false %>
<%= f.label :hobbies, "Reading" %>
<%= f.check_box :hobbies, { multiple: true },"Photography", false %>
<%= f.label :hobbies, "Photography" %>
<%= f.check_box :hobbies, { multiple: true },"Travelling", false %>
<%= f.label :hobbies, "Travelling" %>
</div>
<br>
<div class="addresses">
<%= f.fields_for :addresses do |address| %>
<%= render 'address_fields', f: address %>
<% end %>
</div>
<%= link_to_add_association "add address", f, :addresses, data: {association_insertion_node: '.addresses', association_insertion_method: :append} %>
<%= f.submit "Save Employee", class:"btn-primary border-0 rounded-pill shadow p-3" %>
<% end %>
</div>
</div>
</div>
Now let’s make a partial file with the name “_address_fields.html.erb” in the employee directory.
app/views/employees/_address_fields.html.erb
<!-- Nested Form for f -->
<div class="nested-fields">
<%= f.label :house_number, class: "mt-3" %>
<%= f.number_field :house_number, placeholder: "Enter Your Flat/House Number", class: "mb-3 form-control" %>
<%= f.label :society_name, class: "mt-3" %>
<%= f.text_area :society_name, placeholder: "Enter Your Society Name", class: "mb-3 form-control" %>
<%= f.label :area, class: "mt-3" %>
<%= f.text_area :area, placeholder: "Enter Your Area Name", class: "mb-3 form-control" %>
<%= f.label :city, class: "mt-3" %>
<%= f.text_area :city, placeholder: "Enter Your City Name", class: "mb-3 form-control" %>
<%= link_to_remove_association "Delete", f %>
</div>
The dynamic fields are now displayed in show.html.erb show page as well.
<!-- Dynamic Address Show Here -->
<div>
<% @employee.addresses.each do |address| %>
House Number: <%= address.house_number %> <br>
Society Name: <%= address.society_name %> <br>
Area: <%= address.area %> <br>
City: <%= address.city %> <br>
<% end %>
</div>
This sort of output will be generated by it.
That’s it for implementing a dynamic address field using the cocoon gem.
Preview :
Let’s try to make it more appealing by adding some styling now.
Starting with the “add employee” link from new.html.erb or edit.html.erb
Previous Code :
<%= link_to_add_association "add address", f, :addresses, data: {association_insertion_node: '.addresses', association_insertion_method: :append} %>
Updated Code :
<%= link_to_add_association "add address", f, :addresses, data: {association_insertion_node: '.addresses', association_insertion_method: :append} %>
Additionally, replace the Previous _address_fields with this.
_address_fields.html.erb
<!-- Nested Form for f -->
<div class="nested-fields">
<div class="row">
<div class="col">
<%= f.label :house_number, class: "mt-3" %>
<%= f.number_field :house_number, placeholder: "Enter Your Flat/House Number", class: "mb-3 form-control" %>
</div>
<div class="col">
<%= f.label :society_name, class: "mt-3" %>
<%= f.text_field :society_name, placeholder: "Enter Your Society Name", class: "mb-3 form-control" %>
</div>
</div>
<div class="row">
<div class="col">
<%= f.label :area, class: "mt-3" %>
<%= f.text_field :area, placeholder: "Enter Your Area Name", class: "mb-3 form-control" %>
</div>
<div class="col">
<%= f.label :city, class: "mt-3" %>
<%= f.text_field :city, placeholder: "Enter Your City Name", class: "mb-3 form-control" %>
</div>
</div>
<%= link_to_remove_association "Delete", f, class:"btn btn-danger mb-2" %>
</div>
Output :
show.html.erb
<!-- Dynamic Address Show Here -->
<div>
<% @employee.addresses.each do |address| %>
<p>➡ House Number: <%= address.house_number %></p>
<p>➡ Society Name: <%= address.society_name %></p>
<p>➡ Area: <%= address.area %></p>
<p>➡ City: <%= address.city %></p>
<hr>
<% end %>
</div>
Output :
3) Final Preview :
4) References :
My Github Repo: https://github.com/rutikkpatel/Cocoon-Gem-Rails
Cocoon Gem Repo: https://github.com/nathanvda/cocoon