ruby-on-railsnested-formshas-many-throughfields-for

Rails 5 - has_many through: and nested fields_for in forms


I am new to RoR (using rails 5) and have problems with a has_many through: association.

I want to create Categories with different labels for different Languages.

Here is my model:

class Language < ApplicationRecord
  has_many :category_infos
  has_many :categories, through: :category_infos
end

class Category < ApplicationRecord
  has_many :category_infos
  has_many :languages, through: :category_infos
  accepts_nested_attributes_for :category_infos
end

class CategoryInfo < ApplicationRecord
  belongs_to :language
  belongs_to :category
  accepts_nested_attributes_for :language
end

The controller:

class CategoriesController < ApplicationController

  def new
    @category = Category.new
    @languages = Language.all
    @languages.each do |language|
      @category.category_infos.new(language:language)
    end
  end

  def create
    @category = Category.new(category_params)
    if @category.save
      redirect_to @category
    else
      render 'new'
    end
  end

  private
    def category_params
      params.require(:category).permit(:name, category_infos_attributes:[:label, language_attributes: [:id, :language]])
    end

end

The form:

<%= form_with model: @category, local: true do |form| %>
  <% if @category.errors.any? %>
    <div id="error_explanation">
      <h2>
        <%= pluralize(@category.errors.count, "error") %>:
      </h2>
      <ul>
        <% @category.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
    <br/>
  <% end %>
  <p>
    <%= form.label :name %>
    <%= form.text_field :name %>
  </p>
  <p>
    Labels:
  </p>
  <table>
    <% @category.category_infos.each do |category_info| %>
      <tr>
        <td>
          <%= category_info.language.name %>
        </td>
        <td>
          <%= form.fields_for :category_infos, category_info do |category_info_form| %>
            <%= category_info_form.fields_for :language, category_info.language do |language_form| %>
              <%= language_form.hidden_field :id, value: category_info.language.id %>
              <%= language_form.hidden_field :name, value: category_info.language.name %>
            <% end %>
            <%= category_info_form.text_field :label %>
          <% end %>
        </td>
      </tr>  
    <% end %>
  </table>
  <p>
    <%= form.submit %>
  </p>
<% end %>

When I create a new category, I get this error :

Couldn't find Language with ID=1 for CategoryInfo with ID=

on Line :

@category = Category.new(category_params)

However I already have registered several languages in database (1 = English, 2 = French etc...)

How do I need to write the form so that I can create a Category and its CategoryInfos in English, French etc... at the same time?

Thanks in advance for your answers


Solution

  • You're making a classic newbie misstake and using fields_for when you just want to create an association by passing an id.

    <%= form_with model: @category, local: true do |f| %>
    
      # ... 
    
      <%= f.fields_for :category_infos do |cif| %>
        <%= cif.collection_select(:language_id, Language.all, :name, :id) %>
        <%= cif.text_field :label %>
      <% end %>
    <% end %>
    

    While you could also pass the attributes to let a users create languages at the same time its very much an anti-pattern as it adds a crazy amount of responsibilities to a single controller. It will also create an authorization problem if the user is allowed to create categories but not languages.

    Use ajax to send requests to a seperate languages controller instead if you need the feature.