Creating Nested Forms in Rails With Jquery

Techie     October 2022

Definition

A nested form is a form within a form. We use it to handle multiple models in a single form. In this example, we have a todo_list that contains many tasks. A nested form will allow us to perform CRUD operations on the todo_list & tasks models within a single form.


Prerequisites

  1. Ruby 3.2.0
  2. Rails 7.0.3


Part A: Creating a Rails 7 Project

Create a new rails project called devise_email by following this document: Creating A Rails 7 App: With esbuild, bootstrap and jquery.


Part B: Creating the models

1 . Create the todo_list.rb and task.rb models

i). Generate the models

 $ rails g model todo_list
 
 $ rails g model task 


ii). Edit the files

# models/todo_list.rb

class TodoList < ApplicationRecord

  has_many :tasks, dependent: :destroy
  
end


# models/task.rb

class Task < ApplicationRecord

  belongs_to :todo_list
  
end


2 . Create migrations

i). Edit the migrations

# db/migrate/20221002112836_create_todo_lists.rb

class CreateTodoLists < ActiveRecord::Migration[7.0]
  def change
    create_table :todo_lists do |t|
      t.string :name

      t.timestamps
    end
  end
end


# db/migrate/20221002114011_create_tasks.rb

class CreateTasks < ActiveRecord::Migration[7.0]
  def change
    create_table :tasks do |t|
      t.string :name
      t.boolean :completed
      t.date :date
      t.integer :todo_list_id      

      t.timestamps
    end
    add_index :tasks, :todo_list_id    
  end
end


ii) Run migrations

$ rails db:migrate


Part C: Creating the Controller

1 . Create the todo_lists controller

i). Generate the controller

 

$ rails g controller todo_lists


ii). Replace the code in the file with this code

 
# controllers/todo_lists_controller.rb

class TodoListsController < ApplicationController

  before_action :set_todo_list, only: [:show, :edit, :update, :destroy]


  def index
    @todo_lists = TodoList.all
  end
  
  
  def new
    @todo_list = TodoList.new
  end


  def edit 
    @todo_list = TodoList.find(params[:id])
  end
 
 
  def update
    respond_to do |format|
      if @todo_list.update(todo_list_params)
        format.html { redirect_to [:main, @todo_list], notice: 'To Do List was successfully updated.' }
        format.json { head :no_content }
      else
        format.html { render action: 'edit' }
        format.json { render json: @todo_list.errors, status: :unprocessable_entity }
      end
    end
  end
  
  
  def create
  
    ActiveRecord::Base.transaction do 
      
      todo_list = params[:todo_list_id] == "new" ? TodoList.new : TodoList.find(params[:todo_list_id])
      todo_list.name = params[:todo_list_name]      
        
      if !params[:tasks].nil?
        todo_list.tasks.destroy_all if todo_list.persisted?
      end
        
      if !params[:tasks].nil?   
      params[:tasks].each do |weekly_attendance|
        atts = CGI::parse weekly_attendance
      
        new_task = todo_list.tasks.build(
          id: atts['task_id'][0],
          name: atts['task_name'][0],
          completed: atts['task_status'][0],
          date: atts['due_date'][0],
          todo_list_id: atts['todo_list_id'][0],          
          created_at: Time.now,
          updated_at: Time.now      
        )
      end
      end

    
      if todo_list.save!
        render json: {id: todo_list.id}
        flash[:notice] = "To Do List successfully saved."
      else
        render json: false
      end
              
    end    
  end


  def destroy
    TodoList.find(params[:id]).destroy
    redirect_to todo_lists_path, notice: "Deleted Successfully."
  end

   
  private 
      # Use callbacks to share common setup or constraints between actions.
    def set_todo_list
      @todo_list = TodoList.find(params[:id])
    end


   def todo_list_params
     params.require(:todo_list).permit(:name, :created_at, :updated_at)
   end
  
end


2 . Edit the routes.rb file to ook like this

# config/routes.rb

Rails.application.routes.draw do
  root "home#index"
  resources :todo_lists
end


Part D: Creating the Views

1 . Edit the view templates

i). Create the new.html.erb template and add this code

 
<!-- views/todo_lists/new.html.erb -->

<div class="container mt-3">
  <div class ="row">
    <h1>New To Do List</h1>

    <%= render 'form' %>


    <div class ="col-1"><%=link_to "Back", todo_lists_path, class: "btn btn-primary btn-sm btn-info pull-right" %></div>

  </div>
</div>


ii). Create the _form.html.erb partial template and add this code

 
<!-- views/todo_lists/_form.html.erb -->

<div class="col-md-6" style="padding-top: 70px; margin: auto">
  <table class="table">
    <thead>
      <tr>
        <th>Name</th>
      </tr>
    </thead>
    <tbody class="fields">
     <tr>
      <% if @todo_list.persisted? %>
      <td><input type="hidden" id="todo_list_id" value="<%= @todo_list.id %>"><input type="text" id="todo_list_name" value="<%= @todo_list.name %>"></td>
      <% else %>
      <td><input type="hidden" id="todo_list_id" value="new"><input type="text" id="todo_list_name"></td>      
      <% end %>
     <tr>
    </tbody>
  </table>


  <table class="table tasks">
    <thead>
      <tr>
        <th></th>                    
        <th>Task</th>              
        <th>Status</th>
        <th>Due</th>
      </tr>
    </thead>
    <tbody class="fields">
      <% @todo_list.tasks.each do |t| %>    
     <tr class="task-table-row">
      <td><center><button type="button" class="btn btn-danger btn-sm remove-row pull-left blockable"> <i class="fa fa-minus-circle"></i></button></center></td>      
      <td><input type="text" class="form-control" name="task_name" value="<%= t.name %>"></td>
      <td>
       <select readonly name="task_status" class="form-control">
        <option value="<%= t.completed %>"><%= t.completed? ? "Completed" : "Pending"%></option>
        <option value='<%= t.completed? ? 0 : 1 %>'><%= t.completed ? "Pending" : "Completed"%></option>
       </select>
      </td> 
      <td><input type="date" class="form-control" name="due_date" value="<%= t.date %>"></td>                                      
     </tr>
      <% end %>
    </tbody>
  </table>
  
  <div class ="container">
   <div class="pull-right">
  <a class="btn btn-primary task add-row pull-left">
    <span class="icon"><i class="fa fa-plus"></i></span>
    <span>Add Task</span>
  </a>
  </div>

 <div class="mt-4">
  <button type="button" class="btn btn-success btn-sm todo-list float-right" id="save_todo_list">Save</button> 
 </div>
 </div>

</div>


iii). Create the edit.html.erb template and add this code

 
<!-- views/todo_lists/edit.html.erb -->

<div class="container mt-3"><br><br>
  <div class="row">
  <div><%=link_to "View", [@todo_list ], class: "btn btn-primary btn-sm btn-info pull-right" %></div>
    <h1 style="text-align: center">Editing To Do List</h1>
    
    <%= render "form", todo_list: @todo_list %>

  </div>
</div>


iv). Create the show.html.erb template and add this code

 
<!-- views/todo_lists/show.html.erb -->

<div class="col-md-4" style="padding-top: 70px; margin: auto">
  <table class="table">
    <thead>
      <tr>
        <th>Name</th>
      </tr>
    </thead>
    <tbody class="fields">
     <tr>
      <td><%= @todo_list.name %></td>
     <tr>
    </tbody>
  </table>


  <table class="table tasks">
    <thead>
      <tr>
        <th>Task</th>              
        <th>Status</th>
        <th>Due</th>
      </tr>
    </thead>
    <tbody class="fields">
      <% @todo_list.tasks.each do |t| %>    
     <tr class="task-table-row">
      <td><%= t.name %></td>
      <td><%= t.completed? ? "Completed" : "Pending" %></td> 
      <td><%= t.date %></td>                                      
     </tr>
      <% end %>
    </tbody>
  </table>
  
  <div class ="container">
   <div class="pull-right">
  <a class="btn btn-primary task add-row pull-left" href="/todo_lists/<%=@todo_list.id%>/edit">
    <span class="icon"><i class="fa fa-pen"></i></span>
    <span>Edit</span>
  </a>
  </div>

 <div class="mt-4">
  <button type="button" class="btn btn-success btn-sm todo-list float-right" id="save_todo_list">Save</button> 
 </div>
 </div>
</div>


2 . Add fontawesome in the head tag of application.html.erb

 
<head>
<!-- views/application.html.erb -->

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A==" crossorigin="anonymous" referrerpolicy="no-referrer" />

<!-- ... -->    
</head>
<!-- ... -->


Part E: Creating the JavaScript File

1 . Create the todo_lists.js file in javascript/src directory and add this code

 
// javascript/todo_lists.js


  $("button#save_todo_list").click(function(){

  var tasksData = [];

  todoListID = $("#todo_list_id").val();
  todoListName = $("#todo_list_name").val();
  
  $(".task-table-row").each(function(index){
  
    var serialized_data = $(this).find('.form-control').serialize();
  
    tasksData.push(serialized_data);

  });

      $.post("/todo_lists",
              {tasks: tasksData, 
               todo_list_id: todoListID,
               todo_list_name: todoListName                 
               }, 
             function(result){
               console.log("Server Result: " + JSON.stringify(result));
             
               if(result == false){
                 window.location.href = "/todo_lists/" 
               }
               else{
               window.location.href = "/todo_lists/" + result.id + "/edit"
               };

      });
            
  });


    $(".task.add-row").click(function () {

      tableBody = $('table.tasks tbody');

      taskRow = "<tr class='task-table-row'><td><center><button type='button' class='btn btn-danger btn-sm remove-row pull-left'> <i class='fa fa-minus-circle'></i></button></center></td><td><input name='task_id' type='hidden' class='form-control'></input><input name='todo_list_id' type='hidden' class='form-control'></input><input type='text' name='task_name' class='form-control'></td><td><select name='task_status' class='form-control'><option value='1'>Completed</option><option value='0'>Pending</option></select></td><td><input type='date' name='due_date' class='form-control'></td></tr>";
    
      $(taskRow).appendTo(tableBody)
    });        


    $(document).on('click','.remove-row',function(){
        $(this).parents('tr').remove();
    });


2 . Import the todo_lists.js in javascript/application.js

 
// javascript/application.js

// ...

import "./src/todo_lists"


Final Results

Navigate to http://localhost:3000/todo_lists/new and test it.

missing image


missing image


Thanks for reading, see you in the next one!