Create multi-image upload with liveview and add descriptions to each image

I’m struggling to create a form where the user should be able to upload multiple images and add descriptions for each of them. The idea is to add them as embeds to a resource (in this case a post resource).

defmodule MyApp.Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    field :content, :string
 
    embeds_many :images, Image
  end
end

defmodule MyApp.Post.Image do
  use Ecto.Schema

  embedded_schema do
    field :image_url, :string # For simplicity. In reality it's a waffle upload
    field :description, :string
  end
end

So far I set up a basic form with a live file upload, that shows a preview of the uploaded files, but I don’t know how to progress from here. Any ideas?

<div phx-drop-target={@uploads.images.ref} class="my-2">
  <.h3><%= gettext("Images") %></.h3>
  <.live_file_input class="hidden" upload={@uploads.images} />
  <label
    for={@uploads.images.ref}
    class="flex justify-center items-center p-6 my-2 rounded-lg shadow-sm bg-slate-200 cursor-pointer hover:bg-slate-400"
  >
    <.h4 class="m-0"><%= gettext("Upload images") %></.h4>
  </label>
  <div class="grid grid-cols-2 gap-2">
    <%!-- component to render image previews + form inputs --%>
    <.image
      :for={entry <- @uploads.images.entries}
      field={@form[:images]}
      entry={entry}
      myself={@myself}
    />
  </div>
</div>

Here are the docs—They are detailed and short. I basically forget how to do image uploads every time I start a new project and I essentially copy paste everything on that page and it works beautifully (you just have to change the references to my_app). I’ve never used Waffle, though.

Did you find a satisfying solution?

Sorry for digging this out a year later, but I’m at a similar position and my post is not receiving feedback.

Thanks!

You can do something like the below to create dynamic input fields:

  <.input
    id={"description_#{entry.ref}"}
    field={@form[:"description_#{entry.ref}"]}
    placeholder="Description"
    phx-update="ignore"
  />

Then just use the values similar to the below:

  defp assign_media_params(entries, params, ref_positions) do
    for entry <- entries do
      new_ref = Map.get(ref_positions, entry.ref, String.to_integer(entry.ref))
      |> Integer.to_string()

      %{
        ref:           new_ref,
        url_original:  Path.join(S3Upload.s3_host(), S3Upload.s3_key(entry, "full")),
        url_preview:   Path.join(S3Upload.s3_host(), S3Upload.s3_key(entry, "prev")),
        url_blur:      Path.join(S3Upload.s3_host(), S3Upload.s3_key(entry, "blur")),
        privacy:       params["privacy"],
        warning:       Map.get(params, "warning_#{new_ref}", "SFW"),


        description:   Map.get(params, "description_#{new_ref}",    "")
      }
    end
  end

The new_ref is something external. I can change the order of images in the socket and create new refs so they appear in that order. You would just use the default entry.ref if you want to do it in the standard order.

In the end we decided to make a separate view where you can sort the images and add descriptions.

For future reference, your forms’ fields don’t have to mirror your Ecto schemas, the one you persist. You can have specialised data structures for your views and translate them to params for your business logic layer and then translate to Ecto schemas.