Overwriting the image with phoenix liveview upload on edit

I have followed Chris’s tutorial to do file uploads with LiveView, working great but he only explores the case of creating immutable media.

Ideal scenario: Editing the form and selecting a new image should replace the image, allow me to delete the old image on success. It doesn’t seem possible with this setup.

photo entry is not part of the changeset because we manipulate it manually.

I can’t check if old category.photo != newcategory.photo at any stage.

Photo will upload but category.photo won"t change because it isn’t part of the changeset.

defp save_category(socket, :edit, category_params) do
    category = put_photo_url(socket, socket.assigns.category)

    case Recipes.update_category(category, category_params, &consume_photos(socket, &1)) do
def update_category(%Category{} = category, attrs, after_save \\ &{:ok, &1}) do
    IO.inspect(category)

    category
    |> Category.changeset(attrs)
    |> Repo.update()
    |> after_save(after_save)
    |> IO.inspect()
  end
defp consume_photos(socket, %Category{} = category) do
    consume_uploaded_entries(socket, :photo, fn meta, entry ->

I have been without proper sleep for a while, am I missing something?

How do you guys handle this?

I came up with a hacky workaround like this but for some reason, photo field will not update in the database. losing my marbles over here! It is included and reflected in the changeset.


  defp save_category(socket, :edit, category_params) do
    old_photo = socket.assigns.category.photo
    category = put_photo_url(socket, socket.assigns.category)

    # This should only change if there is ever an upload!
    if category.photo && old_photo != category.photo do
      IO.inspect("I HIT HERE")
      IO.inspect(old_photo)
      category_params = category_params |> Map.put_new("photo", category.photo)
      IO.inspect(category_params)
    end

    case Recipes.update_category(category, category_params) do
      {:ok, newcategory} ->
        IO.inspect(newcategory)
        consume_photos(socket, newcategory)
        remove_photos(old_photo)
  def update_category(%Category{} = category, attrs) do
    category
    |> Category.changeset(attrs)
    |> Repo.update()
  end

field is definitely included in the changeset

  def changeset(category, attrs) do
    category
    |> cast(attrs, [:order, :name, :photo])
    |> validate_required([:order, :name])
  end

you may want to move this outside your if:

    category_params = 
   if category.photo && old_photo != category.photo do
      IO.inspect("I HIT HERE")
      IO.inspect(old_photo)
      category_params |> Map.put_new("photo", category.photo)
      IO.inspect(category_params)
    end

This has to do with scoping rules. Basically, the category_params in your case call is the exact same one you got as the 3rd parameter. The category_params inside the if is in the if's scope, not the function’s scope, thus the re-binding inside the if’s scope had no effect outside.

https://elixir-lang.readthedocs.io/en/latest/technical/scoping.html

Try this in the console:

iex> var = "this is var"
iex> if true, do: var = "this is another var"
iex> IO.inspect(var)
iex> "this is var"
1 Like

thank you but didn’t make any difference

What’s happening in put_photo_url? If you are manually setting the photo field there, then when you pass the struct to the changeset, it won’t see it as a change.

Only the fields present in the changes part of the changeset are sent to the database. Any other, in-memory changes done to the schema are ignored.
Ecto.Repo — Ecto v3.6.1

1 Like

Thank you, lifesaver didn’t spot that obvious thing!

The whole tutorial felt a bit hacky so in the end.I followed ecto way I think a better path without dirtying the context too.

Now I can update the picture if selected and delete the old one as necessary in a tidier manner.

defp save_category(socket, :edit, category_params) do
    category_params = put_photo_url(socket, category_params)

    case Recipes.update_category(socket.assigns.category, category_params) do
      {:ok, new_category} ->
        consume_photos(socket)

        if socket.assigns.category.photo != new_category.photo do
          remove_photos(socket.assigns.category.photo)
        end
defp save_category(socket, :new, category_params) do
    category_params = put_photo_url(socket, category_params)

    case Recipes.create_category(category_params) do
      {:ok, _category} ->
        consume_photos(socket)
defp put_photo_url(socket, params) do
  ...

  Map.put_new(params, "photo", List.first(urls))
end

The reset is reverted to the previous state before the tutorial

2 Likes