Active record nested attributes allow us to build nested forms. In most cases, you may need a way to populate your relation for accepts_nested_attributes_for on the fly. In this example, we look at how we can add items on the fly using turbo.
The main idea is to handle form submission implictly. In our form we add an extra submit button.
<%= form_with(model: [:e7, order]) do |form| %>
<% form.object.items.each_with_index do |item, index| %>
<%= form.fields :items_attributes, model: item, index: index do |f| %>
<fieldset>
<%= f.collection_select :product_id, Product.all, :id, :name, {}, class: "" %>
<%= f.text_field :quantity %>
<%= f.button :_destroy, value: true, formaction: new_e7_order_path, formmethod: "get" do %>
Remove
<% end %>
</fieldset>
<% end %>
<% end %>
<%= form.fields :items_attributes, index: form.object.items.size do |f| %>
<%= f.button :_destroy, value: false, formaction: new_e7_order_path, formmethod: "get" do %>
Add item
<% end %>
<% end %>
<%= form.submit %>
<% end %>
In this button Add item we override submission by using formaction. By default, a form is a POST HTTP method but again we also override this with formmethod
When we click Add new a get request is made to new_order_path. The button has a name _detroy and value value, this adds an extra key in the params hash which will build a new item that gets added to our list, The cycle continues as new items get added.
{
"authenticity_token"=>"[FILTERED]",
"order"=>{
"items_attributes"=>{
"0"=>{
"product_id"=>"397",
"quantity"=>"1"
},
"1"=>{
"_destroy"=>"false"
}
}
}
}
To remove an item we use a similar button but for this case destroy value will be true. This will add a destroy: true attribute to the removed item, rails will handle this and remove this item from the relationship.
<%= f.button :_destroy, value: true, formaction: new_e7_order_path, formmethod: "get" do %>
Remove
<% end %>
The controller is very simple as we only initialize a new Order with the permitted parameters. On page load the items will be blank that's why we are adding at least one item if items are blank.
class OrdersController < ExampleController
def new
@order = Order.new(order_params)
@order.items.build if @order.items.blank?
end
private
def order_params
params.fetch(:order, {}).permit(items_attributes: [:product_id, :quantity, :_destroy])
end
end
To take advantage of turbo we wrap item attributes and Add new button inside a <turbo_frame> and target it with Add new and Remove get request, this way only that part of the page is reloaded.