ruby-on-railshas-many-throughnested-form-forruby-on-rails-5

Create 3rd level child in a single form - Rails 5


I'm building a simple top-to-bottom Workout Routine app on ROR. I'm able to create a Workout Day (parent) and an Exercise (child) on the same form. But I can't seem to save the Weighted Set (grandchild) when I submit the form. The interesting thing is that since the Exercise is saved, I can go to that exercise edit page, add a Weighted Set, and the Weighted Set will show up in the Workout Day show page. I think it has to do with the Weighted Set not being associated with the Exercise at the time of creation. How cam I tie wll three models together? I know I'm close!

I have the whole app on github. I the link isn't working, try this URL https://github.com/j-acosta/routine/tree/association

Models

class WorkoutDay < ApplicationRecord
  has_many :exercises, dependent: :destroy
  has_many :weighted_sets, through: :exercises

  accepts_nested_attributes_for :exercises
  accepts_nested_attributes_for :weighted_sets
end

class Exercise < ApplicationRecord
  belongs_to :workout_day, optional: true
  has_many :weighted_sets, dependent: :destroy

  accepts_nested_attributes_for :weighted_sets
end

class WeightedSet < ApplicationRecord
  belongs_to :exercise, optional: true
end

Workout Day Controller

class WorkoutDaysController < ApplicationController
  before_action :set_workout_day, only: [:show, :edit, :update, :destroy]

  ...

  # GET /workout_days/new
  def new
    @workout_day = WorkoutDay.new

    # has_many association .build method => @parent.child.build
    @workout_day.exercises.build

    # has_many :through association .build method => @parent.through_child.build
    # @workout_day.weighted_sets.build
    @workout_day.weighted_sets.build

  end

  ...

  # POST /workout_days
  # POST /workout_days.json
  def create
    @workout_day = WorkoutDay.new(workout_day_params)

    respond_to do |format|
      if @workout_day.save
        format.html { redirect_to @workout_day, notice: 'Workout day was successfully created.' }
        format.json { render :show, status: :created, location: @workout_day }
      else
        format.html { render :new }
        format.json { render json: @workout_day.errors, status: :unprocessable_entity }
      end
    end
  end

  ...

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

    # Never trust parameters from the scary internet, only allow the white list through.
    def workout_day_params
      params.require(:workout_day).permit(:title, exercises_attributes: [:title, :_destroy, weighted_sets_attributes: [:id, :weight, :repetition]])
    end
end

New Workout Day form

<%= form_for @workout_day do |workout_day_form| %>
  <% if workout_day.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(workout_day.errors.count, "error") %> prohibited this workout_day from being saved:</h2>

      <ul>
      <% workout_day.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div>
    <%= workout_day_form.label :title, 'Workout Day Name' %>
    <%= workout_day_form.text_field :title %>
  </div>


  exercise_field will go here
  <div>
    <%= workout_day_form.fields_for :exercises do |exercise_field| %>
        <%= exercise_field.label :title, 'Exercise' %>
        <%= exercise_field.text_field :title %>
    <% end %>
  </div>

  weighted_set_fields will go here
  <div>
      <%= workout_day_form.fields_for :weighted_sets do |set| %>
        <%= render 'exercises/weighted_set_fields', f: set %>
      <% end %>  
  </div>

  <div>
    <%= workout_day_form.submit %>
  </div>
<% end %>

Solution

  • The culprit is the workout_day_params. In the form you have the fields of weighted_sets nested under the workout_day. But in the workout_day_params, you have weighted_sets_attributes under exercises_attributes which is the reason for your problem. Changing it to below should solve the issue.

    def workout_day_params
      params.require(:workout_day).permit(:title, exercises_attributes: [:title, :_destroy], weighted_sets_attributes: [:id, :weight, :repetition])
    end
    

    ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrMany‌​Reflection

    This due to wrong associations. You should consider tweaking your associations like below

    class WorkoutDay < ApplicationRecord
      has_many :weighted_sets, dependent: :destroy
      has_many :exercises, through: :weighted_sets
    
      accepts_nested_attributes_for :exercises
      accepts_nested_attributes_for :weighted_sets
    end
    
    class Exercise < ApplicationRecord
      has_many :weighted_sets, dependent: :destroy
      has_many :workout_days, through: :weighted_sets
    end
    
    class WeightedSet < ApplicationRecord
      belongs_to :exercise, optional: true
      belongs_to :workout_day, optional: true
    end