Search code examples
ruby-on-railsvalidationnested-attributes

Ruby on Rails - Problem with nested_attributes validation when allow_destroy: true


I have some model with accepts_nested_attributes_for

class SomeModel < ApplicationRecord
  has_many :some_resources
  
  accepts_nested_attributes_for :some_resources, allow_destroy: true
    
  ...

In View it looks something like that

<%= form_with(model: @some_model, ...) do |form| %>
...
<%= form.fields_for :some_resources do |some_resource_form| %>
  <table>
    <tbody>  
    <%= some_resource_form.hidden_field :_destroy, value: '0' %>
    <tr>
      <th>
        <%= some_resource_form.label :some_field_1, 'Some field 1' %>
      </th>
      <td>
        <%= some_resource_form.text_field :some_field_1 %>
      </td>
    </tr>
    <tr>
      <th>
        <%= some_resource_form.label :some_field_2, 'Some field 2' %>
      </th>
      <td>
        <%= some_resource_form.text_field :some_field_2 %>
      </td>
    </tr>
    </tbody> 
  </table>
<% end %>
...
<% end %>

In Controller

def new
  3.times do |i|
    @some_model.some_resources.build
  end
end

Config

...
config.active_record.index_nested_attribute_errors = true

When drawing a page, it looks like that

<table>
  <tbody>
    <input value="0" type="hidden" name="some_model[some_resources_attributes][0][_destroy]" id="some_model_some_resources_attributes_0__destroy">

    <tr>
      <th>
        <label for="some_model_some_resources_attributes_0_some_field_1">Some field 1</label>
      </th>
      <td>
        <input type="text" name="some_model[some_resources_attributes][0][some_field_1]" id="some_model_some_resources_attributes_0_some_field_1">
      </td>
    </tr>

    <tr>
      <th><label for="some_model_some_resources_attributes_0_some_field_2">Some field 2</label></th>
      <td>
        <input type="text" name="some_model[some_resources_attributes][0][some_field_2]" id="some_model_some_resources_attributes_0_some_field_2">
      </td>
    </tr>
  </tbody>
</table>  

<table>
  <tbody>
    <input value="0" type="hidden" name="some_model[some_resources_attributes][1][_destroy]" id="some_model_some_resources_attributes_1__destroy">

    <tr>
      <th>
        <label for="some_model_some_resources_attributes_1_some_field_1">Some field 1</label>
      </th>
      <td>
        <input type="text" name="some_model[some_resources_attributes][1][some_field_1]" id="some_model_some_resources_attributes_1_some_field_1">
      </td>
    </tr>

    <tr>
      <th><label for="some_model_some_resources_attributes_1_some_field_2">Some field 2</label></th>
      <td>
        <input type="text" name="some_model[some_resources_attributes][1][some_field_2]" id="some_model_some_resources_attributes_1_some_field_2">
      </td>
    </tr>
  </tbody>
</table>   

<table>
  <tbody>
    <input value="0" type="hidden" name="some_model[some_resources_attributes][2][_destroy]" id="some_model_some_resources_attributes_2__destroy">

    <tr>
      <th>
        <label for="some_model_some_resources_attributes_2_some_field_1">Some field 1</label>
      </th>
      <td>
        <input type="text" name="some_model[some_resources_attributes][2][some_field_1]" id="some_model_some_resources_attributes_2_some_field_1">
      </td>
    </tr>

    <tr>
      <th><label for="some_model_some_resources_attributes_2_some_field_2">Some field 2</label></th>
      <td>
        <input type="text" name="some_model[some_resources_attributes][2][some_field_2]" id="some_model_some_resources_attributes_2_some_field_2">
      </td>
    </tr>
  </tbody>
</table>      

Exists ability to create only selected objects using the _destroy property. When validation is triggered, errors has the following structure

#<ActiveModel::Errors:0x00007f4f695a5720 @base=#<SomeModel id: nil, label: "Label", ..., created_at: nil, updated_at: nil, deleted_at: nil>,
@messages={
:"some_resources[0].some_field_1"=>["can't be empty"], :"some_resources[0].some_field_2"=>["can't be empty"],
:"some_resources[1].some_field_1"=>["can't be empty"], :"some_resources[1].some_field_2"=>["can't be empty"], 
:"some_resources[2].some_field_1"=>["can't be empty"], :"some_resources[2].some_field_2"=>["can't be empty"]
}, 
@details={
:"some_resources[0].some_field_1"=>[{:error=>:blank}], :"some_resources[0].some_field_2"=>[{:error=>:blank}], 
:"some_resources[1].some_field_1"=>[{:error=>:blank}], :"some_resources[1].some_field_2"=>[{:error=>:blank}], 
:"some_resources[2].some_field_1"=>[{:error=>:blank}], :"some_resources[2].some_field_2"=>[{:error=>:blank}]
}>

The problem arises when I, for example, do not want to save the second object (with field name="some_model[some_resources_attributes][1][some_field_1]"). To do this, I put for the parameter _destroy value "1". After that, the errors includes the following

#<ActiveModel::Errors:0x00007f4f627d4d18 @base=#<SomeModel id: nil, label: "Label", ..., created_at: nil, updated_at: nil, deleted_at: nil>,
@messages={
:"some_resources[0].some_field_1"=>["can't be empty"], :"some_resources[0].some_field_2"=>["can't be empty"],
:"some_resources[1].some_field_1"=>["can't be empty"], :"some_resources[1].some_field_2"=>["can't be empty"]
}, 
@details={
:"some_resources[0].some_field_1"=>[{:error=>:blank}], :"some_resources[0].some_field_2"=>[{:error=>:blank}], 
:"some_resources[1].some_field_1"=>[{:error=>:blank}], :"some_resources[1].some_field_2"=>[{:error=>:blank}]
}>

As can be seen, the errors of nested attributes contain only the sequence number of the object in errors. Therefore field name="some_model[some_resources_attributes][0][some_field_1]" have index 0 in errors name="some_model[some_resources_attributes][1][some_field_1]" is missing in errors name="some_model[some_resources_attributes][2][some_field_1]" have index 1 in errors

Because of such behavior is not possible to draw the error message in the correct field. I think the problem is very common. But I did not find anything. Is there any solution and what better practices exist to implement such a functional? Thank you!

P.S. I use Rails 6.0.3.6


Solution

  • This is an odd behaviour. I solution could be adding a custom error. According to Rails documentation https://guides.rubyonrails.org/active_record_validations.html#working-with-validation-errors you can add a custom error to your validation, The add method creates the error object by taking the attribute, the error type and additional options hash.

    In your model:

    attr_accessor :input_field_index
    
    validate do |resource|
      errors.add :attribute_name, :custom_error_name, message: "is blank", options: {input_field_index: 2}
    end
    

    In your view add a hidden_field with the input field index. Also remember to allow it in strong_params of your controller.

    You should be able to get the error with the input_field_index parameter, then you can customize it in your view.