ruby-on-railsruby-on-rails-5simple-formaccepts-nested-attributes

How do I prevent multiple simple_fields_for forms showing up?


I have a simple_fields_for form that is rendered within an iterator, like so:

<%= simple_form_for @port_stock, url: port_stocks_sell_order_path, method: :post, html: { class: "form-inline" } do |f| %>

  <% @buy_port_stocks.each do |port_stock| %>
    <%= f.simple_fields_for :closed_positions, html: { class: "form-inline" } do |c| %>
      <div class="form-group">
        <%= c.input_field :port_stock_id, as: :hidden, value: port_stock.id %>
        <%= c.input_field :num_units, id: "sell-ps-#{port_stock.id}", placeholder: "Number of Units", class: "form-control mx-sm-3" %>
         <%= c.input_field :closed_price, id: "sale-price-for-ps-#{port_stock.id}", placeholder: "Sale Price", class: "form-control mx-sm-3" %>
       </div>
   <% end %>
  <% end %>
<% end %>

In my controller I have this:

@port_stock = current_user.port_stocks.friendly.find(params[:id])
@buy_port_stocks = current_user.port_stocks.buy.joins(:stock).where(stocks: { ticker: @stock.ticker})
@cp = @port_stock.closed_positions.build

My PortStock.rb model:

  has_many :closed_positions, dependent: :destroy
  accepts_nested_attributes_for :closed_positions, allow_destroy: true

My ClosedPosition.rb model:

class ClosedPosition < ApplicationRecord
  belongs_to :closer, class_name: "PortStock", foreign_key: "closer_id"
  belongs_to :closed, class_name: "PortStock", foreign_key: "port_stock_id" 
end

The above works perfectly for the @port_stock records that have no closed_positions.

For example, that form is rendered like this:

good-rendering-of-simple_form_for

Note that the Number of Units and Sale Price field only show up once in each row (which is what I expect).

However, once I create a closed_position on any PortStock it creates two issues:

First Issue

It pre-fills the existing closed position as a field, and then renders another blank field for closed_positions, i.e. like this:

first-issue

What I want to happen is for it to just render the new form and not re-render the existing closed_position values on each row. The user should not be able to edit existing closed positions in this form.

Second Issue

Whenever there are multiple closed positions, it renders the wrong ones in every row.

wrong-values-rendered-for-each-closed-position

Note how each value that is rendered says num_units: 100 && price: 8.0, look at the console output of those same closed_positions:

=> [#<ClosedPosition:0x00007ff13e77c6d0
  id: 9,
  closer_id: 2,
  port_stock_id: 17,
  num_units: 100,
  closed_price: 8.0,
  ticker: "CAC",
 #<ClosedPosition:0x00007ff13e77c2e8
  id: 10,
  closer_id: 3,
  port_stock_id: 18,
  num_units: 10,
  closed_price: 7.95,
  ticker: "CAC",
 #<ClosedPosition:0x00007ff13e77c018
  id: 11,
  closer_id: 10,
  port_stock_id: 19,
  num_units: 50,
  closed_price: 7.9,
  ticker: "CAC",

The correct values are actually:

  1. Num Units: 100 && Price: 8.0
  2. Num Units: 10 && Price: 7.95
  3. Num Units: 50 && Price: 7.9

I don't understand why it outputs the same value for all the port_stock objects.

How do I fix these two issues in my simple_fields_for form?


Solution

  • OK, let me coach this with my logic isn't solid, but I'm basing it on ActionView::Helpers::FormHelper

    Regardless of simple_form or standard Rails helpers, there's a difference between passing an associated record and the associated model.

    <%= form_for @record %>
      <%= fields_for @associated_record %>
    

    vs.

    <%= form_for @record %>
      <%= fields_for :associated_model %>
    

    And I believe it has to do with instantiating a new record when one isn't present.

    So, my first swing in the comments was to suggest using the more specific record variable:

    :closed_positions @cp

    Like thus:

    <%= simple_form_for @port_stock, url: port_stocks_sell_order_path, method: :post, html: { class: "form-inline" } do |f| %>
      <% @buy_port_stocks.each do |port_stock| %>
        <%= f.simple_fields_for @cp, html: { class: "form-inline" } do |c| %>
          ...
    

    As you commented, this isn't a perfect solution, but does solve the duplicate fields.

    But, as your controller indicates, @cp isn't a direct association to @buy_port_stocks, which is what the form is looping through.

    So, the solution you really discovered is to declare both, which is allowed:

    Fields may reflect a model object in two ways - how they are named (hence how submitted values appear within the params hash in the controller) and what default values are shown when the form the fields appear in is first displayed. In order for both of these features to be specified independently, both an object name (represented by either a symbol or string) and the object itself can be passed to the method separately

    And the docs suggest declaring both, which in your case would look like this:

    <%= simple_form_for @port_stock, url: port_stocks_sell_order_path, method: :post, html: { class: "form-inline" } do |f| %>
      <% @buy_port_stocks.each do |port_stock| %>
        <%= f.simple_fields_for :closed_positions, @cp, html: { class: "form-inline" } do |c| %>
          ...
    

    While I will obviously take the wealth and fame associated with the correct answer, it's only fair to admit that I didn't get you there alone.