How to handle order of uploaded images in a LiveView edit page?

So, I’m making a system that works like an online store where the user can create new “products”, these products can have images, and the order of these images matter since it is the order that it will be shown in the product page.

When I created the LiveView page to create/add a new product, I added support to upload images for that product. I also wanted to allow the user to change the order in which these images will be stored.

To do that, I just update the upload assign entries list changing the order of the items, and that will reflect in the order in which the images are shown and the order they will be consumed.

This works fine, but now I need to create the edit property page, and there I’m not sure what is the best approach to achieve the same thing.

The problem that I see is that on the edit page, I will have already uploaded images from the property and new images added by the user during the edit. This means that now I have two separate lists, but I still want to show them as one single list and allow the user to change the order of the images even if some of them are not uploaded yet.

In other words, I have this:

new_to_be_uploaded_images = [a, b, c] # This is the entries list that liveview will add when we enable uploads
already_stored_old_images = [d, e] # This is the list of already uploaded images for the product

But I want to have a way to store their new order, ideally, they would be in the same list, for example:

order = [b, e, a, c, d]

What I thought as a solution is to have a new list that has some reference to the other two lists, let’s say that I also store their UUIDs, then I could have a separated list with their UUIDs and I use that list to show the images in the liveview:

order = [{:new, "some-uuid"}, {:old, "some-uuid"}, {:old, "some-uuid"}, ...]

I think that would work, but seems like some extra work, so I was wondering if there is a better solution for my specific case.

Could you share a few more details about your data model and how you’re storing images? Is it directly on the “products” as a list of uuids/links or through a has many association?

With LiveView, why not append new placeholder entries to your existing entries assign and keep the view logic simple? If you need to figure out which ones are newly added when it comes to handling the update or validate events on the server side, just compare the list of persisted entries to the list of updated entries e.g. via MapSet.difference/2.

I’m saving the images to S3 and storing the URLs in an array field called images_urls inside the products table.

As a placeholder, do you mean manually creating Phoenix.LiveView.UploadEntry structs for each image that is already stored so I can place them also inside the entries list of uploads?

To be honest, I thought about that, but I’m not sure exactly how I would do that since I don’t know what would be the correct approach to create an Phoenix.LiveView.UploadEntry manually in a way that LiveView would know that the image is already consumed (maybe that one is easy since there is a done? field in that struct) and how to make the component <.live_img_preview show the correct image preview for them.

So, for now, this is my current solution:

First I created a UploadedEntry struct which contains information about an already uploaded/stored entry:

defmodule UploadedEntry do
  @type t :: %__MODULE__{
    uuid: String.t(),
    url: String.t(),
    client_name: String.t(),
    client_type: String.t(),
    progress: non_neg_integer
  }
  
  defstruct [:uuid, :url, :client_name, :client_type, progress: 100]

  def new(uuid, url, client_name, client_type) do
    struct(__MODULE__, uuid: uuid, url: url, client_name: client_name, client_type: client_type)
  end 
end

Then, I created a module called ImageUploads which will handle all the logic, this module will store what are my current entries in the current order and will handle updates, deletions, etc:

defmodule ImagesUploads do
  alias Phoenix.LiveView.UploadEntry

  @type entry :: UploadEntry.t() | UploadedEntry.t()

  @type t :: %__MODULE__{
    max_entries: non_neg_integer,
    entries: [entry],
    uploaded_entries: [UploadedEntry.t()],
    deleted_uploaded_entries: [UploadedEntry.t()]
  }

  defstruct [:max_entries, entries: [], uploaded_entries: [], deleted_uploaded_entries: []]

  @spec new([UploadedEntry.t()], Keyword.t()) :: t
  def new(uploaded_entries \\ [], opts \\ []) do
    max_entries = Keyword.get(opts, :max_entries, 1)

    struct(__MODULE__, max_entries: max_entries, entries: uploaded_entries, uploaded_entries: uploaded_entries)
  end

  @spec update_upload_entries(t, [UploadEntry.t()]) :: t
  def update_upload_entries(images_uploads, updated_entries) do
    %{entries: entries, uploaded_entries: uploaded_entries, deleted_uploaded_entries: deleted_entries} =
      images_uploads

    updated_entries = updated_entries ++ uploaded_entries

    deleted_entries = deleted_entries ++ (entries -- updated_entries)
    added_entries = updated_entries -- entries

    entries = (entries -- deleted_entries) ++ added_entries

    %{images_uploads | entries: entries}
  end

  @spec remove_upload_entry(t, UploadEntry.t()) :: t
  def remove_upload_entry(images_uploads, entry) do
    %{entries: entries} = images_uploads

    %{images_uploads | entries: entries -- [entry]}
  end

  @spec remove_uploaded_entry(t, UploadedEntry.t()) :: t
  def remove_uploaded_entry(images_uploads, entry) do
    %{entries: entries, uploaded_entries: uploaded_entries, deleted_uploaded_entries: deleted_entries} = images_uploads

    cond do
      entry in deleted_entries ->
        images_uploads

      entry not in uploaded_entries ->
        images_uploads

      true ->
        %{images_uploads | entries: entries -- [entry], deleted_uploaded_entries: deleted_entries ++ [entry]}
    end
  end

  @spec get_errors(t) :: [:too_many_files]
  def get_errors(%{entries: entries, max_entries: max_entries}) do
    errors = []

    errors = errors ++ if Enum.count(entries) > max_entries, do: [:too_many_files], else: []

    errors
  end 
end

Now, from the LiveView side, I just need to store a struct of the ImagesUploads in my state. During mount, I will fetch the current product images, convert them to a UploadedEntry struct and create the ImagesUploads struct with it.

After that, I can use update_upload_entries to update the upload entries during a “validate” event, remove_upload_entry during an event to remove a UploadEntry (cancel_upload), and remove_uploaded_entry during an event to remove a UploadedEntry.

Then, when I’m ready to submit, I will just read the entries field from ImagesUploads, consume the UploadEntry structs and then use the deleted_uploaded_entries field to delete already stored images.

What is missing here is the logic to reorder entries inside the entries field, but this is very specific to each implementation, so I decided to paste it here.

1 Like