Search code examples
ruby-on-railsformssimple-formnested-forms

Rails has_many :through nested forms with simple form


I am trying to make a player character generator. I have a form that hopefully will allow me to attach skills with their values to a character sheet model. I made models like this:

class CharacterSheet < ApplicationRecord
  has_many :character_sheet_skills, dependent: :destroy
  has_many :skills, through: :character_sheet_skills
  belongs_to :user

  accepts_nested_attributes_for :skills
end

class Skill < ApplicationRecord
  has_many :character_sheet_skills, dependent: :destroy
  has_many :character_sheets, through: :character_sheet_skills

  attr_reader :value
end

class CharacterSheetSkill < ApplicationRecord
  belongs_to :skill
  belongs_to :character_sheet
end

Character sheet model holds data about player character and skill model has all skills available in game. In CharacterSheetSkill I'd like to store the skills that the player chooses for his character together with an integer field setting the skill value.

When opening form, I already have a full list of skills in database. All I want to do in form is create a character sheet that has all of these skills with added value. I tried using "fields_for" in form, but I couldn't really get that to work. Right now it looks like this:

<%= simple_form_for [@user, @sheet] do |f| %>
    <%= f.input :name %>
    <%= f.input :experience, readonly: true, input_html: {'data-target': 'new-character-sheet.exp', class: 'bg-transparent'} %>
...
    <%= f.simple_fields_for :skills do |s| %>
      <%= s.input :name %>
      <%= s.input :value %>
    <% end %>
<% end %>

How can I make that form so it saves character sheet together with CharacterSheetSkills?


Solution

  • A better idea here is to use skills as a normalization table where you store the "master" definition of a skill such as the name and the description.

    class CharacterSheetSkill < ApplicationRecord
      belongs_to :skill
      belongs_to :character_sheet
      delegate :name, to: :skill
    end
    

    You then use fields_for :character_sheet_skills to create rows on the join table explicitly:

    <%= f.fields_for :character_sheet_skills do |cs| %>
      <fieldset>
        <legend><%= cs.name %></legend>
        <div class="field">
          <%= cs.label :value %>
          <%= cs.number_field :value %>
        </div>
        <%= cs.hidden_field :skill_id %>
      </fieldset>
    <% end %>
    

    Instead of a hidden fields you could use a select if you want let the user select the skills.

    Of course nothing will show up unless you "seed" the inputs:

    class CharacterSheetController < ApplicationController
      def new
        @character_sheet = CharacterSheet.new do |cs|
          # this seeds the association so that the fields appear
          Skill.all.each do |skill|
            cs.character_sheet_skills.new(skill: skill)
          end
        end
      end
    
      def create
        @character_sheet = CharacterSheet.new(character_sheet_params)
        if @character_sheet.save
          redirect_to @character_sheet
        else
          render :new
        end
      end
    
      private
      def character_sheet_params
        params.require(:character_sheet)
              .permit(
                 :foo, :bar, :baz,
                 character_sheet_skill_attributes: [:skill_id, :value]
              )
      end
    end