Home > Turbo frame, nested attributes form

Turbo frame, nested attributes form

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.