Delete embed_many

I’m at a loss on how to delete an embed_many record from my LiveView page. I have an embedded schema called Quote and it has embeds_many :features, Feature, on_replace: :delete. After struggling with it I arrived at:

@doc """
  Delete feature from the features list.
  """
  def handle_event("feature_delete", %{"value" => index_string}, socket) do
    quotation = socket.assigns.form.data
    case Integer.parse(index_string) do
      {index, _} ->
        features = Enum.reject(quotation.features, fn f -> f.id == index end)

        changeset = Quote.changeset(quotation)
                    |> Ecto.Changeset.put_embed(:features, features)

        data = Ecto.Changeset.apply_action!(changeset, :update)

        changeset = Quote.changeset(data)

        socket = assign(socket, :form, to_form(changeset))
        {:noreply, socket}

      :error ->
        {:noreply, socket}
    end
  end

I have no delusions that my code probably sucks – I don’t really get changesets that well, especially outside of using them with a database. Even so the above code seems to work EXCEPT my page’s <.inputs_for :let={f} field={quote[:features]}> doesn’t update properly. Say I have three features and I go to delete the middle one, instead of removing it, the second feature becomes identical to the third and the third remains on the page.

Hi,
It looks like you’re trying to delete an element from an embedded list using the element’s “id” field. However, the “id” field is not unique for each element in the list, but rather corresponds to the element’s index in the list. As a result, when you try to delete an item using its “id”, you end up deleting the wrong item.

To solve this problem, you can try deleting the element using its index in the list rather than its id. You can obtain the index of the element in the list using the Enum.with_index/1 function, then use this information to delete the correct element.

def handle_event("feature_delete", %{"value" => index_string}, socket) do
  quotation = socket.assigns.form.data
  case Integer.parse(index_string) do
    {index, _} ->
      # Utiliser Enum.with_index pour obtenir l'index de chaque élément
      features = quotation.features |> Enum.with_index() |> Enum.reject(fn {f, i} -> i == index end) |> Enum.map(&elem(&1, 0))

      changeset = Quote.changeset(quotation)
                  |> Ecto.Changeset.put_embed(:features, features)

      data = Ecto.Changeset.apply_action!(changeset, :update)

      changeset = Quote.changeset(data)

      socket = assign(socket, :form, to_form(changeset))
      {:noreply, socket}

    :error ->
      {:noreply, socket}
  end
end

@nseaSeb I can see why you would you think that, but when I add a feature, I set all the ids to their current index. So your solution works out exactly the same.

The underlying issue has something to do with how LiveView and Changesets work in relation to the form rendering. Everything works except my deleted entry ends up as duplicate of the entry below it on the form (not in the underlying data).

While I still don’t quite get it 100%, I found an online tutorial with which I was able to get it working (and clean up my code) by following it’s instruction.

The key to fixing the issue seems to be adding a virtual delete field with a hidden form input and inserting a procedure in the changeset function that tells the changeset to perform a delete action when that field is set to true.

The guide also uses a class to hide (or 50% opacity in this case) the line when the delete field is true – however that part didn’t actually do anything for me. My feature line disappears instantly, maybe because my change event applies the changeset every time it fires, instead of just doing it once on submit.

Hi,
OK, I see. Sorry about that, and I’m glad you found a solution.

Not at all. Thanks for giving it a look! I very much appreciate it.

1 Like