I have a LiveView stateful component used to upload files. It uses an Ecto schema to structure things (not backed by a DB). There is an array of files in the changeset and I would like to be able to change the name of a file and add an optional message after it has been uploaded but before the form is saved.
If I use <.inputs_for :let={file} field={@form[:files]}> it works as expected but I would like to address the individual files like if I hypothetically had written <.input_for :let={file} field={@form[:files][index]}> where index is the number of the file in the array or key in the map. In other words, I’d like to move the file1 component (two inputs and a button) inside the for loop generating the article elements instead of putting it outside of it using <.inputs_for>. The idea is to tie the controls to each individual uploaded file and not put them all below all the uploads.
The problem of course is that I cannot index the array or map to get the individual files and if I try to do so by writing field={@form[:files][0]} or field={@form[:files]["0"]} I get an access error: Phoenix.HTML.FormField does not implement the Access behaviour. I don’t even know how to write the .input component using its name attribute.
How could I put <.file1 file={file} myself={@myself}/> inside the loop ?
defmodule MaintenanceWeb.MenuLive.Actions.Autre do
use MaintenanceWeb, :live_component
use Ecto.Schema
import Ecto.Changeset
schema "autre" do
field(:datetime, :naive_datetime)
field(:message, :string)
embeds_many :files, MyFile do
field(:ref, :string)
field(:name, :string)
field(:message, :string)
end
end
def render(assigns) do
~H"""
<div>
<p>Inside H Component MaintenanceWeb.MenuLive.Actions.Autre</p>
<.simple_form id="form" for={@form} phx-change="validate" phx-submit="save" phx-target={@myself}>
<.input type="datetime-local" field={@form[:datetime]}/>
<.input type="textarea" field={@form[:message]}/>
<.live_file_input upload={@uploads.files} multiple/>
<section id="uploaded-files">
<h2>Uploaded Files (<%= length(@uploaded_files) %>)</h2>
<%= for {path, entry} <- @uploaded_files do %>
<article class="uploaded-files-entry">
<div class="preview">
<%= case mime2type!(entry.client_type) do %>
<% "image" -> %>
<img class="preview" src={path}/>
<% "audio" -> %>
<audio class="preview" controls preload="metadata" src={path}>Your browser does not support the <code>audio</code> element.</audio>
<% "video" -> %>
<video class="preview" controls muted preload="metadata" src={path}>Your browser does not support the <code>video</code> element.</video>
<% "_" -> %>
<p>N/A</p>
<% end %>
</div>
</article>
<% end %>
<.inputs_for :let={file} field={@form[:files]}>
<.file1 file={file} myself={@myself}/>
</.inputs_for>
</section>
<:actions>
<.button phx-disable-with="Saving...">Save</.button>
</:actions>
</.simple_form>
</div>
"""
end
def file1(assigns) do
~H"""
<.input type="text" class="uploaded-file-name" field={@file[:name]}/>
<.button type="button" id="remove-upload" phx-target={@myself} phx-click="remove-upload" phx-value-index={@file.index} aria-label="remove">❌</.button>
<.input type="textarea" field={@file[:message]} placeholder="Description optionnelle du fichier"/>
"""
end
I’m not sure if I’m following correctly…are you asking how you can split the inputs out so you have the files listed in one place of your form, and the name fields elsewhere?
If that’s the question, then one thing you can do is use multiple inputs_for. In one just have the files, in the other just have the name input.
One other note: you’re using this to remove the files:
As of a recent version of Ecto, you can instead use the drop param to simplify this. No need to write your own custom handle_event for removal. Additionally, when iterating through a collection with inputs_for, the form will provide an index field for you to use:
Hmm, not sure what the best practice would be, but you could try using something along the lines of :if={file.ref == entry.ref} to only show the related file.
Alternatively, you could decorate each element of the @uploaded_files list assign to include the form data when you consume_uploaded_entries and append to @uploaded_files such that you can do something like for {path, entry, file_data} <- @uploaded_files.
Could you share the data structure of @uploaded_files and show how its currently set?
@uploaded_files just contains the list of tuples {path, entry} received by consume_uploaded_entry but with the new path to the copied file. That is the information I get from the uploads. On the other hand, I also have an Ecto.Changeset which embeds the name and message of all the files given by the form on the web page.
I think I will just structure the HTML code more or less like below. And use a little bit of CSS to have the uploaded files previews side by side with the HTML inputs to change their name and add an optional message (or comment). I have added a hidden input to be able to link each file in @form[:files] to its matching entry in @uploaded_files.