Hi,
I’m learning Phoenix and I’m currently trying to create a nested form similar to the one in the guide about contexts. So far I have the following schemas:
schema "carts" do
belongs_to :user, User
has_many :items, CartItem
timestamps(type: :utc_datetime)
end
schema "cart_items" do
field :quantity, :integer
field :price_when_added, :decimal
belongs_to :cart, Cart
belongs_to :product, Product
timestamps(type: :utc_datetime)
end
What I’m stuck at is creating the nested form so I can update the quantities of many cart items using a single form submission. The example in the guide is using the inputs_for
component to iterate over the cart items and display inputs, however, I want to use the table
component that is included when generating a new project with phx.new
. It takes in a list of rows as an attribute so the inputs_for
component doesn’t solve my problem.
Here’s my current template:
<.form :let={f} for={@changeset}>
<.header>
Cart
<:subtitle :if={Enum.empty?(@cart.items)}>Your cart is empty</:subtitle>
<:actions>
<.button>Update Cart</.button>
</:actions>
</.header>
<.table id="cart_items" rows={how_do_i_get_my_rows_as_fields?}>
<:col :let={item} label="Name">{item.product.name}</:col>
<:col :let={item} label="Price">{Decimal.round(item.price_when_added, 2)}</:col>
<:col :let={item} label="Quantity">{item.quantity}</:col>
<:action :let={item}>
<.link href={~p"/cart_items/#{item.product}"} method="DELETE">
Delete
</.link>
</:action>
</.table>
</.form>
<.back navigate={~p"/products"}>Back to products</.back>
How do I get f[:items]
as a list of form fields and use it in place of how_do_i_get_my_rows_as_fields?
?
<.inputs_for>
does it’s own iteration, because it inserts hidden fields per record, so you cannot use it with <.table>
, which also iterates rows to handle identification of table rows for LV stream support. These are not easily composable like that. You can manually combine the code powering those two components though. Personally I’d opt for a table component, which works on a lower level and allows the caller to deal with building individual table rows.
1 Like
How I tackled this was moving the <tr>
into its own phoenix component (table_row
), which will use an inputs_for
rather than :for
if a %Phoenix.HTML.FormField{}
is passed to the rows
of the table.
For example:
<table class="table table-zebra">
<thead>
<tr>
<th :for={col <- @col}>{col[:label]}</th>
<th :if={@action != []}>
<span class="sr-only">{gettext("Actions")}</span>
</th>
</tr>
</thead>
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
<.table_row :let={row} rows={@rows} row_id={@row_id}> <%!-- !!!this was modified!!! --%>
<td
:for={col <- @col}
phx-click={@row_click && @row_click.(row)}
class={@row_click && "hover:cursor-pointer"}
>
{render_slot(col, @row_item.(row))}
</td>
<td :if={@action != []} class="w-0 font-semibold">
<div class="flex gap-4">
<%= for action <- @action do %>
{render_slot(action, @row_item.(row))}
<% end %>
</div>
</td>
</.table_row>
</tbody>
</table>
def table_row(%{rows: %Phoenix.HTML.FormField{}} = assigns) do
~H"""
<.inputs_for :let={row} field={@rows}>
<tr id={@row_id && @row_id.(row)}>
{render_slot(@inner_block, row)}
</tr>
</.inputs_for>
"""
end
def table_row(assigns) do
~H"""
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
{render_slot(@inner_block, row)}
</tr>
"""
end
1 Like
Thanks for the idea!
I ended up doing something very similar. The only difference is that I used inputs_for
and a normal for comprehension so I don’t have to repeat the <tr />
tag twice.
attr :rows, :list, required: true
slot :inner_block
defp table_rows(%{rows: %Phoenix.HTML.FormField{}} = assigns) do
~H"""
<.inputs_for :let={row} field={@rows}>
{render_slot(@inner_block, row)}
</.inputs_for>
"""
end
defp table_rows(assigns) do
~H"""
<%= for row <- @rows do %>
{render_slot(@inner_block, row)}
<% end %>
"""
end
1 Like