I am helping my brother in his first forays into Elixir and Phoenix.
He is rewriting a Rails app. His customer wants the interfaces to stay the same.
The form he is struggling is this.
You have an Entry
which has many Product
s, where these represent variants for the same entry. The second part is a Cart
, this contains a list of Product
with a reference to the current user.
The interface is built up by showing general information from Entry
and a list of the Product
variants. Each variant needs an input
to enter the chosen quantity the customer wants to order. The information here coming from the Cart
, not every Product
has a record in Cart
.
Now for the issue.
We need to loop over @entry.products
to show the information about that specific variant, but we also need to get the current quantity out of the @cart.cart_items
.
I tried to create a form that contains the CartItem
s and filled in the :product
association. That works on first render, but as soon as something is entered in one of the fields, they all get turned into Ecto.Changeset
and gone is the product information.
In the end I created a very ugly beast that loops over @entry.products
, renders the inputs with a name like "product[#{product.id}][quantity]"
and a get_data
function that is called inside the loop (yikes) and checks to see if @form
already contains data, if so get it from there, if not get it from the cart.
Then in the save
handle_event
, I get a map with the product_id
as keys and the individual fields as values and convert this into a list of maps so Ecto
can cast_assoc
it.
It is ugly as hell, it works, but I cannot help but wonder if there isn’t a better/cleaner solution that I somehow missed.
Any input would be appreciated.
<.form for={@form} phx-change="validate" phx-submit="save" id="cart_form">
<table>
<tr :for={product <- @entry.products}>
<% data = get_data(@form, @entry, product, @cart) %>
<td>
<input type="number" value={data.quantity} name={"product[#{product.id}][quantity]"} />
<input type="hidden" value={data.id} name={"product[#{product.id}][id]"} />
<input
type="hidden"
value={data.product_id}
name={"product[#{product.id}][product_id]"}
/>
<input
type="hidden"
value={data.catalog_id}
name={"product[#{product.id}][catalog_id]"}
/>
</td>
<td><%= product.article_number %></td>
...
def handle_event("validate", %{"product" => cart_params}, socket) do
form =
cart_params |> to_form()
{:noreply, socket |> assign(form: form)}
end
def handle_event("save", %{"product" => cart_params}, socket) do
%{cart: cart} = socket.assigns
cart_items =
Enum.reduce(cart.cart_items, [], fn item, agg ->
if data = Map.get(cart_params, to_string(item.product_id)) do
if data["quantity"] == "0" do
agg
else
[data | agg]
end
else
[Map.from_struct(item) | agg]
end
end) ++
Enum.reduce(cart_params, [], fn {_key, item}, agg ->
if item["quantity"] == "0" || item["id"] != "0" do
agg
else
[Map.delete(item, "id") | agg]
end
end)
Shop.Cart.update(cart, %{cart_items: cart_items})
{:noreply, socket}
end
def get_data(form, %{catalog_id: catalog_id}, %{id: product_id}, %{cart_items: items}) do
if form.params == %{} do
Enum.find(
items,
%Schema.CartItem{
product_id: product_id,
catalog_id: catalog_id,
quantity: 0,
id: 0
},
fn prod -> prod.product_id == product_id end
)
else
Map.get(form.params, to_string(product_id))
|> Enum.map(fn {k, v} -> {String.to_existing_atom(k), v} end)
|> Enum.into(%{})
end
end
Again it is ugly and any input would be greatly appreciated.