Error when running put_embed when trying to dynamically remove form fields

Hi there! I’m having fun building a recipe site with phoenix, but I’m getting stuck with a dynamic form I have.

I’m following this tutorial, trying to make it so that my form can dynamically add and remove form fields. Currently, I’m trying to make it so that I can delete form fields. My problem is that I can delete a field once but after trying again I get this error:

** (RuntimeError) cannot replace related %Galley.Recipes.RecipeIngredient{id: “6n3WR”, ingredient: “”, measurement: “”, quantity: “”}. This typically happens when you are calling put_assoc/put_embed with the results of a previous put_assoc/put_embed/cast_assoc/cast_embed operation, which is not supported. You must call such operations only once per embed/assoc, in order for Ecto to track changes effeciently

The original tutorial code looks like this:

def handle_event("remove-variant", %{"remove" => remove_id}, socket) do
    variants =
      socket.assigns.changeset.changes.variants
      |> Enum.reject(fn %{data: variant} ->
        variant.temp_id == remove_id
      end)

    changeset =
      socket.assigns.changeset
      |> Ecto.Changeset.put_assoc(:variants, variants)

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

Mine is similar, but the form I’m trying to change is part of an embedded schema:

defmodule Galley.Recipes.Recipe do
  use Ecto.Schema
  import Ecto.Changeset
  alias Galley.Recipes, as: R

  schema "recipes" do
    field :source, :string
    field :title, :string
    field :slug, :string
    field :yields, :string
    embeds_many :steps, R.RecipeStep, on_replace: :delete
    embeds_many :ingredients, R.RecipeIngredient, on_replace: :delete
    embeds_one :time, R.RecipeTime, on_replace: :update

    timestamps()
  end

defmodule Galley.Recipes.RecipeIngredient do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :ingredient, :string
    field :quantity, :string
    field :measurement, :string
  end

  def changeset(step, attrs) do
    step
    |> cast(attrs, [:ingredient, :quantity, :measurement])
    |> validate_required([:ingredient, :quantity])
  end
end

And the code that is trying to remove the deleted field:

  def handle_event("remove-ingredient", %{"remove" => id_to_remove} , socket) do
    IO.inspect(socket, pretty: true)
    ingredients =
      socket.assigns.changeset.changes.ingredients
      |> Enum.reject(fn %{:data => ingredient} ->
        ingredient.id == id_to_remove
      end)
      IO.inspect(ingredients, pretty: true)
    changeset = socket.assigns.changeset |> Ecto.Changeset.put_embed(:ingredients, ingredients)
    {:noreply, assign(socket, changeset: changeset)}
  end

Which results in the error at the top of the post. I’m still a little confused about what might be the proper way to be working with changesets here. Does anybody have any input? I’m hoping I’m just missing a concept here due to my newness to Ecto.
Thanks for any help!

1 Like