Complex Forms in Rails 5

In this article, you will learn how to create a project and a few tasks for the project at the same time. You can watch this tutorial as a screencast Complex Forms in Rails 5.

Steps

Step 1

Create a new Rails 5 project and projects controller with index, new and create actions.

rails new todoist
rails g controller projects index new create

Define the project resource in routes.rb.

Rails.application.routes.draw do
  resources :projects
end

Step 2

The projects controller looks familiar:

class ProjectsController < ApplicationController
  def index
    @projects = Project.all
  end

  def new
    @project = Project.new
  end

  def create
    @project = Project.new(allowed_params)
    if @project.save
      flash[:notice] = 'Successfully created project.'

      redirect_to projects_path
    else
      render :new
    end
  end

  private

  def allowed_params
    params.require(:project).permit(:name)
  end
end

Step 3

Generate a project model with name attribute.

rails g model project name

Step 4

Declare the has_many tasks relationship in the project model.

class Project < ApplicationRecord
  has_many :tasks
end

Declare the belongs_to relationship in the task model.

class Task < ApplicationRecord
  belongs_to :project
end

Step 5

The form to create a new project looks like this:

<h1>New Project</h1>
<%= form_for @project do |f| %>
  <p> 
    Name: <%= f.text_field :name %>
  </p>
  <p>
    <%= f.submit 'Create Project' %>
  </p>
<% end %>

Step 6

Generate task model with name attribute and project_id foreign key.

rails g model task name project_id:integer

Step 7

The projects index page lists all the projects.

<h1>Projects</h1>

<% for project in @projects %>
<%= project.name %>
<% end %>

You can now create a project.

Step 8

Add:

accepts_nested_attributes_for :tasks

to the project model.

class Project < ApplicationRecord
  has_many :tasks

  accepts_nested_attributes_for :tasks
end

When you create a project with some tasks, you will get the error:

error : must exist accepts_nested_attributes_for

Step 9

Add the inverse_of to the task model.

class Task < ApplicationRecord
  belongs_to :project, inverse_of: :tasks
end

Add the inverse_of to the project model.

class Project < ApplicationRecord
  has_many :tasks, inverse_of: :project

  accepts_nested_attributes_for :tasks
end

Read the Rails API to learn about inverse_of.

Step 10

You can now create a project with many tasks. The log file shows that the records were inserted for projects and the tasks.

Started POST "/projects" for ::1 at 2016-04-06 21:02:26 -0700
Processing by ProjectsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"FjkO/a1o3=", "project"=>{"name"=>"Sqare", "tasks_attributes"=>{"0"=>{"name"=>"10"}, "1"=>{"name"=>"20"}, "2"=>{"name"=>"30"}}}, "commit"=>"Create Project"}
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "projects" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "Sqare"], ["created_at", 2016-04-06 23:02:26 UTC], ["updated_at", 2016-04-06 23:02:26 UTC]]
  SQL (0.1ms)  INSERT INTO "tasks" ("name", "project_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "10"], ["project_id", 6], ["created_at", 2016-04-06 23:02:26 UTC], ["updated_at", 2016-04-06 23:02:26 UTC]]
  SQL (0.0ms)  INSERT INTO "tasks" ("name", "project_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "20"], ["project_id", 6], ["created_at", 2016-04-06 23:02:26 UTC], ["updated_at", 2016-04-06 23:02:26 UTC]]
  SQL (0.0ms)  INSERT INTO "tasks" ("name", "project_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "30"], ["project_id", 6], ["created_at", 2016-04-06 23:02:26 UTC], ["updated_at", 2016-04-06 23:02:26 UTC]]
   (0.4ms)  commit transaction
Redirected to http://localhost:3000/projects
Completed 302 Found in 11ms (ActiveRecord: 1.1ms)

Step 11

The form will save tasks with empty value for the task name. Let's fix that now. Add:

reject_if

to the project model.

class Project < ApplicationRecord
  has_many :tasks, inverse_of: :project

  accepts_nested_attributes_for :tasks, reject_if: proc { |attributes| attributes[:name].blank? }
end

Step 12

Display all tasks for a given project in the project index page.

<h1>Projects</h1>

<% for project in @projects %>
  <%= project.name %>
  <% for task in project.tasks %>
    <%= task.name %>
  <% end %>
<% end %>

When you edit an existing project with tasks, it will not work.

Step 13

You must allow id and _destroy to delete tasks.

params.require(:project).permit(:name, tasks_attributes: [:id, :name, :_destroy])

The projects update action looks like this:

def update
  @project = Project.find(params[:id])  
  if @project.update_attributes(allowed_params)
    redirect_to @project, notice: 'Successfully updated project'
  else
    render :edit
  end
end

Step 14

We should also add:

allow_destroy: true

to the project model. It now looks like this:

class Project < ApplicationRecord
  has_many :tasks, inverse_of: :project, dependent: :destroy

  accepts_nested_attributes_for :tasks, reject_if: proc { |attributes| attributes[:name].blank? }, allow_destroy: true
end

Step 15

The project show looks like this:

<%= @project.name %>
<% for task in @project.tasks %>
<%= task.name %>
<% end %>

The new project page looks like this:

<h1>New Project</h1>
<%= form_for @project do |f| %>
  <p>
    Name: <%= f.text_field :name %>
  </p>
  <%= f.fields_for :tasks do |task_form| %>
    <p>
        Task: <%= task_form.text_field :name %>
    </p>
  <% end %>
  <p>
    <%= f.submit 'Create Project' %>
  </p>
<% end %>

Step 16

The project edit page looks like this:

<h1>Edit Project</h1>
<%= form_for @project do |f| %>
  <p>
    Name: <%= f.text_field :name %>
  </p>
  <%= f.fields_for :tasks do |task_form| %>
    <p>
      Task: <%= task_form.text_field :name %>
      <%= task_form.check_box :_destroy %>
      <%= task_form.label :_destroy, "Remove Task" %>
    </p>
  <% end %>
  <p>
    <%= f.submit 'Update Project' %>
  </p>
<% end %>

You will now be able to delete tasks for a given project by going to the project edit page.

Summary

In this article, you learned how to save form fields that is persisted in different tables by using fields_for and nested_attributes_for in Rails 5 apps.

Resources

Cocoon
Covert Coffeescript to Javascript


Related Articles

Watch this Article as Screencast

You can watch this as a screencast Complex Forms in Rails 5


Software Compatibility Best Practices

I spoke to some of the most talented and experienced software developers. I have created a guide that is filled with valuable insights and actionable ideas to boost developer productivity.

You will gain a better understanding of what's working well for other developers and how they address the software compatibility problems.

Get the Guide Now