'How do you persist an instantiated nested form object when creation fails?

I have models Software and Version. A Software has_many Version's and has_one :first_version

class Software < ApplicationRecord
  has_many :versions
  has_one :first_version, -> { order(created_at: :asc) },
                          class_name: "Version", dependent: :destroy 
  accepts_nested_attributes_for :versions
end

class Version < ApplicationRecord
  belongs_to :software
end

I'm building the nested object in the new controller action.

class SoftwaresController < ApplicationController
  
  def new
    @software = current_account.softwares.build
    @software.build_first_version
  end

  def create
    @software = current_account.softwares.build(software_params)
    if @software.save
      redirect_to software_path(@software)
    else
      render :new
    end
  end

  def software_params
    params.require(:software).permit(
      :name,
      first_version_attributes: %i[id release_date],
    )
  end
end

form:

  <%= simple_form_for :software do |f| %>
    <%= f.input :name %>


    <%= f.simple_fields_for :first_version do |v|%>
      <%= v.input :release_date %>
    <% end %>
  <% end %>

With the above code, if something fails during creation, the nested object is persisted even though the object itself and it's parent do not have an id yet, and so errors are displayed under each field with invalid values. At the same time, if I comment out the line where I build the nested object, the form does not break, just no nested fields are displayed. This is good.

Now, because the form is reused in the new and edit views and I don't want to let users edit the :first_version through this form nor rely on the view to render it conditionally if @software.new_record? I put the nested object in a global variable and point the nested form to that variable hoping that the same result will be achieved in the edit view because no global variable will exist.

  def new
    @software = current_account.softwares.build
    @first_version = @software.build_first_version
  end

form:

  <%= simple_form_for :software do |f| %>
    <%= f.input :name %>


    <%= f.simple_fields_for @first_version do |v|%>
      <%= v.input :release_date %>
    <% end %>
  <% end %>

Problem: If something goes wrong during creation the object is no longer persisted and the view breaks due to @first_version being nil. So why is the nested object persisted when I do @parent.build_nested_object but not when @nested_object = @parent.build_nested_object ?



Solution 1:[1]

Solving the problem by creating more i_vars can lead to bugs. I think the best option is to disable the field based on a condition and change your view to the following.

  <%= simple_form_for @software do |f| %>
    <%= f.input :name %>

    <%= f.simple_fields_for @software.first_version || @software.build_first_version do |v| %>
      <%= v.input :release_date, disabled: (true if @software.first_version.id) %>
    <% end %>
  <% end %>

Using this view means that you can initialize only @software on your controller.

class SoftwaresController < ApplicationController
  def new
    @software = current_account.softwares.build
  end
end

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Christos-Angelos Vasilopoulos