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
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.