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 Products, 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 CartItems 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.






















