Search code examples
ruby-on-railsrubyformsnestedmodels

Ruby on Rails: how to make a form for associated models (nested)


First of all I have this:

https://polar-scrubland-30279.herokuapp.com/ - my project which is deployed on heroku (Captain Obvious)

I've got projects and todos inside them. For this moment I show all projects using this way:

------index.html.erb------

<%= render @projects %>

------_project.html.erb-----

<div class="project">
  <div class="project-header">
    <h2><%= project.title %></h2>
  </div>
  <div class="project-todos">
    <% project.todos.all.each do |todo| %>
      <p><%= check_box('tag', todo.__id__, {class: 'icheckbox_square-blue', checked: todo.isCompleted}) %> <%= content_tag :todotext, todo.text %></p>
    <% end %>
  </div>
</div>

And as you understand it doesn't allow me to change my todo's status when checkbox is checked. So that's why I need a form that will allow me to track all the checkboxes. Also I wanna make text-decoration: line-through when checkbox is pressed, but don't get how to.

Is there a way to creat a form which will satisfy my needs? Please can you help me, Any information will be appreciated.

ADDITIONAL INFORAMTION:

GitHub - https://github.com/NanoBreaker/taskmanager

project.rb

class Project < ActiveRecord::Base
  has_many :todos
end

todo.rb

class Todo < ActiveRecord::Base
  belongs_to :project
end

Solution

  • Lets start with the models:

    class Project < ApplicationRecord
      has_many :todos
      accepts_nested_attributes_for :todos
    end
    
    class Todo < ApplicationRecord
      belongs_to :project
    end
    

    accepts_nested_attributes_for lets you create or modify several nested Todo records at once when creating or updating a Project.

    # will update 2 todos at once
    @project.update(
     todos_attributes: [ { id: 1, isComplete: true }, { id: 2, isComplete: false }]
    )
    

    We can use fields_for to create nested inputs for todos:

    <%= f.form_for(@project) do |f| %>
      <%= f.fields_for(:todos) do |tf| %>
        <%= tf.check_box :isCompleted %>
      <% end %>
    <% end %>
    

    This generates fields for todos nested under the key todos_attributes. We can whitelist them by using a hash key containing a array of permitted attributes.

    class ProjectsController < ApplicationController
      before_action :set_project, only: [:show, :edit, :update, :destroy]
    
      def new
        @project = Project.new
        # this seeds the project with 3 empty tasks
        # otherwise we don't have any inputs.
        3.times { @project.todos.new }
      end
    
      def create
        @project = Project.new(project_params)
        if @project.save
          # ...
        else
          # ...
        end
      end
    
      def update
        if @project.update(project_params)
          # ...
        else
          # ...
        end
      end
    
      private 
    
      def set_project
        @project = Project.find(params[:id])
      end
    
      def project_params
        params.require(:project)
          .permit(:foo, :bar, 
            todos_attributes: [:isCompleted, :text]
          )
      end
    end
    

    You can create a form for each project by creating a partial which uses a local instead of an instance variable:

    # app/views/projects/_form.html.erb
    <%= f.form_for(local_assigns[:project] || @project) do |f| %>
      <%= f.fields_for(:todos) do |tf| %>
        <%= tf.check_box :isCompleted %>
      <% end %>
    <% end %>
    
    # app/views/projects/index.html.erb
    <% @projects.each do |project| %>
      <%= render partial: 'projects/form', project: project %> 
    <% end %>
    

    You can reuse the same partial for the other views as well:

    # app/views/projects/new.html.erb
    <%= render partial: 'projects/form' %> 
    
    # app/views/projects/edit.html.erb
    <%= render partial: 'projects/form' %>