Liveview with embedded/nested form does not return value of nested element to handle_event

Hello All,

I have a customer schema with nested contacts that’s defined as an embedded schema that I render in a liveview form component. I’m trying to add and delete the nested contact dynamically. Adding works just fine but when I trigger a remove action, I can’t see the param for the newly added contact in the handle_event callback … not sure how to go about getting this to work properly, any advice on what I’m doing wrong here please?

defmodule MyApp.Customer do
  use Ecto.Schema
  import Ecto.Changeset

  schema "customers" do
    field :name, :string
    embeds_many :contacts, MyApp.ContactDetail

    timestamps()
  end

  @doc false
  def changeset(customer, attrs) do
    customer
    |> cast(attrs, [:name])
    |> cast_embed(:contacts)
    |> validate_required([:name])
  end
end

defmodule MyApp.ContactDetail do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :temp_id, :string, virtual: true
    field :contact_type, Ecto.Enum, values: [:address, :phone, :email]
    field :details, :string
  end

  def changeset(embedded_schema, attrs) do
    embedded_schema
    |> cast(attrs, [:contact_type, :details, :temp_id])
    |> validate_required([:contact_type, :details])
  end
end

In my customer_live/form_component.html.heex, I have something like this for the contacts section:

  <.form
    let={f}
    for={@changeset}
    id="customer-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">

    <%= for c <- inputs_for(f, :contacts) do %>
	    <%= hidden_inputs_for(c) %>
            <%= text_input c, :details %>
            <%= hidden_input(c, :temp_id) %>
            <a href="#"
               phx-target={@myself}
               phx-click="remove-contact"
               phx-value-contact={c.data.temp_id}>&times</a>
    <% end %>

  </.form>

In the customer_live.form_component, the handle_event("remove-contact", %{"contact" => temp_id}, socket) gets called but the temp_id is empty. I tried referencing it like c.temp_id but that blows up with an error.

  def handle_event("add-contact", _, socket) do
    existing_contacts = Map.get(socket.assigns.changeset.changes, :contacts, [])

    new_contacts = [%{temp_id: :rand.uniform(1000)} | existing_contacts]
    changeset = Ecto.Changeset.put_embed(socket.assigns.changeset, :contacts, new_contacts)

    {:noreply, assign(socket, changeset: changeset)}
  end

  def handle_event("remove-contact", %{"contact" => contact_id}, socket) do
    new_contacts = socket.assigns.changeset.changes
    |> Map.get(:contacts)
    |> Enum.reject(&(&1.changes.temp_id == contact_id))

    changeset = Ecto.Changeset.put_embed(socket.assigns.changeset, :contacts, new_contacts)
    {:noreply, assign(socket, changeset: changeset)}
  end

I’m using liveview 0.17.5 btw.

Appreciate any help, thanks!

Quick thought is that the field you’re trying to match on to delete is a virtual field so it doesn’t exist when accessing it. Have you tried matching on the :id rather than the :temp_id for deleting it?

@f0rest8 my understanding is that a virtual field should exist but is just not persisted. I tried the :id field but that seems to reference the Phoenix.HTML.Form interpolated id which is sent back as "customer-form_contacts_0", I have no reference to that before the form is rendered. This is why I thought using a :temp_id set to a random value is maybe a good approach …

and if I try c.data.id I get an empty %{} value back :frowning:

Yea I’m not totally sure but when I’ve used an embedded schema in the form, I did something like this…

<.form let={f} for={@changeset} id="..." phx-target={@myself} phx-change="validate" phx-save="submit">
  <%= for f_item <- inputs_for(f, :line_items) do %>
  ...
    <.button type="button" 
      phx-click="delete-line-item" 
      phx-value-index={f_item.index} 
      phx-target={@myself}>Delete</.button>
  ...
</.form>

# Handle event
def handle_event("delete-line-item", %{"index" => index}, socket} do
  ...
end
1 Like

I was able to get over this issue by utilizing the value of the hidden input field for :temp_id :smiley:. Not sure why I’ve not thought of using that lol

@f0rest8 this is very similar to what you do with referencing the index but I used the value like so:

<%= hidden_input(c, :temp_id) %>
<a href="#"
    phx-target={@myself}
    phx-click="remove-contact"
    phx-value-contact={input_value(c, :temp_id)}>&times</a>
1 Like