Array of changesets in a form? Displaying file upload with interactive filename element parsing and validation

I am using LiveView uploads for file upload and allowing multiple file uploads at once.

Beside the parameters set by allow_upload/3, I need to parse the filename of each file, break it into elements, add elements as editable input fields and validate them against the db scheme. Once the user hits a submit button, everything should be sent.

What I currently have looks something like this:

<.form let={f} for={@changeset} phx-change="validate" phx-submit="save" phx-target={@myself}>
  <div class="row m-0 mb-2">
    <%= live_file_input @uploads.files, class: "form-control rounded-0"%>
  </div>

  <div class="row m-0 mb-2">
    <%= for error <- upload_errors(@uploads.files) do %>
      <div class="alert alert-danger">
        <%= error_to_string(error) %>
      </div>
    <% end %>
  </div>

  <div class="row m-0 mb-2">
    <table class="table table-borderless table-sm">
      <tr>
        <th></th>
        <th>Field1</th>
        <th>Field2</th>
        <th>Field3</th>
        <th>Field4</th>
        <th>Field5</th>
        <th>Field6</th>
        <th>Field7</th>
        <th>Field8</th>
        <th>Field9</th>
      </tr>
      <%= for entry <- @uploads.files.entries do %>
        <%= list_entry(f, entry, assigns)%>
        <%= list_upload_errors(f, entry, assigns)%>
      <% end %>
    </table>
  </div>
</.form>

with helper functions being defined as

  def list_entry(f, entry, assigns) do
    parsed_filename = parse_filename(entry.client_name)
    ~H"""
      <tr>
        <td class="p-0" style="vertical-align: middle">
          <button class="btn btn-primary p-0 d-flex justify-content-center align-items-center rounded-0" type="button" phx-target={@myself} phx-click="cancel_upload" phx-value-ref={entry.ref}>
            <%= Bootstrap.Icons.x_lg(width: 20, height: 20, style: "color: white") %>
          </button>
        </td>
        <td class="p-0 ps-1 pe-1" style="vertical-align: middle"><%= entry.client_name %></td>
        <td class="p-0"><%= text_input f, :field1, value: parsed_filename.field1, style: "width: 100%;" %></td>
        <td class="p-0"><%= text_input f, :field2, value: parsed_filename.field2, style: "width: 100%;" %></td>
        <td class="p-0"><%= text_input f, :field3, value: parsed_filename.field3, style: "width: 100%;" %></td>
        <td class="p-0"><%= text_input f, :field4, value: parsed_filename.field4, style: "width: 100%;" %></td>
        <td class="p-0"><%= text_input f, :field5, value: parsed_filename.field5, style: "width: 100%;" %></td>
        <td class="p-0"><%= text_input f, :field6, value: parsed_filename.field6, style: "width: 100%;" %></td>
        <td class="p-0"><%= text_input f, :field7, value: parsed_filename.field7, style: "width: 100%;" %></td>
        <td class="p-0"><%= text_input f, :field8, value: parsed_filename.field8, style: "width: 100%;" %></td>
      </tr>
    """
  end

  def list_upload_errors(f, entry, assigns) do
    file_errors_present = length(upload_errors(assigns.uploads.files, entry)) > 0
    input_errors_present = length(assigns.changeset.errors) > 0

    ~H"""
      <%= if file_errors_present || input_errors_present do %>
        <tr>
          <td></td>
          <%= if file_errors_present do %>
            <td class="alert alert-danger">
              <%= for error <- upload_errors(@uploads.files, entry) do %>
                <%= error_to_string(error) %>
              <% end %>
            </td>
          <% else %>
            <td></td>
          <% end %>
          <td class="alert alert-danger"><%= error_tag f, :field1 %></td>
          <td class="alert alert-danger"><%= error_tag f, :field2 %></td>
          <td class="alert alert-danger"><%= error_tag f, :field3 %></td>
          <td class="alert alert-danger"><%= error_tag f, :field4 %></td>
          <td class="alert alert-danger"><%= error_tag f, :field5 %></td>
          <td class="alert alert-danger"><%= error_tag f, :field6 %></td>
          <td class="alert alert-danger"><%= error_tag f, :field7 %></td>
          <td class="alert alert-danger"><%= error_tag f, :field8 %></td>
        </tr>
        <tr>
          <td class="p-1" colspan="100%"></td>
        </tr>
      <% end %>
    """
  end

What you can notice is that I am trying to list errors for each entry. However, my main problem is that I would have to compare it against multiple changeset results. This is not compatible with form and I am not sure what would be the best way to proceed about this?

I have come across nested forms with input_for, however, looking at the Nested model forms with Phoenix LiveView tutorial, it looks like I would have to have database schema designed in a way to have cast_assoc in there, with has_many relationship. Since I am just doing an upload action which itself should not be remembered in db, there is no has_many associacion really.

One thing that just crossed my mind is to write a dummy Ecto Model Upload which could be associated with multiple files. The Upload itself would, however never be stored into DB.

Following on the above proposed solution, I have indeed managed to solve this and have made a small example for anyone who finds themselves with a similar problem :slight_smile:

2 Likes