Sumak
Sumak

Reputation: 1061

Rails 6: Dynamic nested forms: get id of nested resource selected by fields_for and collection_select

If you're facing a similar issue you might found interesting (re)reading this posts:


I'm trying to implement nested forms with several models and a dynamically added collection_select field, based on this great tutorial https://www.driftingruby.com/episodes/nested-forms-from-scratch

Sadly, I currently have trouble to retrieve the Product.id from my controller to save the record appropriately.

In my schema:

When I trigger a submission with multiples OrderProduct records, only the last :product_id is passed:

Started PATCH "/formulars/1/orders/53"
Parameters: 
  {
    "authenticity_token"=>"[FILTERED]",
    "product_id"=>{"id"=>"7"},                         # <- :product_id of the last collection_select
    "order"=>
      {
        "order_products_attributes"=>
          {
            "1612773831378"=>{"quantity"=>"1",         # <- each OrderProduct dynamically added
                              "_destroy"=>"false"},    # <-
            "1612774917407"=>{"quantity"=>"6",         # <- frontend JS substitutes the associated 
                              "_destroy"=>"false"},    # <- objects' :object_id with a unique 
            "1612774918874"=>{"quantity"=>"4",         # <- timestamp.
                              "_destroy"=>"false"}}},  # <- I need to access :product_id from here,
          }                                            #    for each OrderProduct added
       },
    "commit"=>"Modifier ce(tte) Commande", 
    "formular_id"=>"1",
    "id"=>"53"                                         # <- Order's :id
  }

Here's how the field is dynamically prepend to the form:

# app/views/orders/_form.html.erb

<%= form_with(model: [formular, order] do |form |%>                        # <- Form for an Order binded to a Formular (nested routes)
  ....
  <%= link_to_add_product_row('Add an article', form, :order_products) %>  # <- Call a Helper method
<% end %>
# app/helpers/application_helper.rb

def link_to_add_product_row(name, f, association, **args)
  new_object = f.object.send(association).klass.new     # <- create a new instance of the associated object
                                                        # f.object   == #<Order id: 53> in this context
                                                        # new_object == #<OrderProduct id: nil, order_id: nil, product_id: nil, quantity: 1>

  id = new_object.object_id                             # <- get its unique :object_id

  fields = f.fields_for(association, new_object, child_index: id) do |builder|
    render(association.to_s.singularize, f: builder)    # 'orders/_order_product' is a partial shown below
  end
  link_to(name, '#', class: "add_fields " + args[:class], data: {id: id, fields: fields.gsub("\n",'')})
end
# app/views/orders/_order_product.html.erb

<tr>
  #
  # I need this collection_select to:
  #   - display the Product's name
  #   - display only Products associated to the selected Formular
  #   - pre-select the Product record, if set
  #   - return the Product.id to the backend
  #
  <td><%= collection_select(:product_id, :id, @formular.products, :id, :name) %></td>
  <td></td>
  <td><%= f.number_field :quantity, hide_label: true %></td>
  <td><%= f.submit %></td>
  <td>
    <%= f.hidden_field :_destroy, as: :hidden, method: :delete %>
    <%= link_to 'Delete', '#', class: 'remove_order_product' %>
  </td>
</tr>
$(document).on('turbolinks:load', function() {

  $('form').on('click', '.add_fields', function(event) {
    var regexp, time;
    time   = new Date().getTime();
    regexp = new RegExp($(this).data('id'), 'g');
    $('.fields').append($(this).data('fields').replace(regexp, time));
    return event.preventDefault();
  });
});
# app/controllers/orders_controller.rb

# before_action callback executed on :create and :update
# goal is to retrieve OrderProducts from params, and associate
# them to the current instance of Order

def set_order_products
  order_products_params = order_params.dig(:order, :order_products_attributes)

  if order_products_params
    order_products_params.each_value do |param|
      product = Product.find(param['product_id'])
      @order.order_products.build(product:  product,
                                  quantity: param['quantity'])
    end
  end
end

# If OrderProducts params were like:
#
#   {"TIMESTAMP"=>{ 
#      "product_id"=>"42",         # <- this parameter is missing
#      "quantity"=>"3",
#      "_destroy"=>"false"}}
#
# then I'd be able to identify to which product this OrderProduct is associated to

Let me know if you want mode code.


Any idea about how to achieve this goal ? I've tried adding hidden_fields, tweaking collection_select, using a select and options_from_collection_for_select / options_for_select; but I must admit that I'm lost (and I may have overcomplexified my schema).

TL_DR: How can I retrieve a Product's ID from a field_for(:order_products) dynamically generated inside Order's controller ?

Upvotes: 0

Views: 589

Answers (1)

Sumak
Sumak

Reputation: 1061

From Can someone explain collection_select to me in clear, simple terms? https://stackoverflow.com/a/8908298/11509906

zquares said:

With regards to using form_for, again in very simple terms, for all tags that come within the form_for, eg. f.text_field, you dont need to supply the first (object) parameter. This is taken from the form_for syntax.

I changed my collection_select from:

<%= collection_select(:product_id, :id, @formular.products, :id, :name) %>

# Parameters: {"authenticity_token"=>"[FILTERED]", "product_id"=>{"id"=>"7"}, "order"=>{"order_products_attributes"=>{"1612782399659"=>{"quantity"=>"3", "_destroy"=>"false"}, "1612782403591"=>{"quantity"=>"5", "_destroy"=>"false"}}}, "commit"=>"Modifier ce(tte) Commande", "formular_id"=>"1", "id"=>"53"}

to:

<%= f.collection_select(:product_id, @formular.products, :id, :name) %>

# Parameters: {"authenticity_token"=>"[FILTERED]", "order"=>{"order_products_attributes"=>{"1612782084184"=>{"product_id"=>"3", "quantity"=>"3", "_destroy"=>"false"}, "1612782085172"=>{"product_id"=>"33", "quantity"=>"-3", "_destroy"=>"false"}}}, "commit"=>"Modifier ce(tte) Commande", "formular_id"=>"1", "id"=>"53"}

So now I can fetch the associated product within Order's controller

Upvotes: 0

Related Questions