Dynamic File Uploads using LiveView

I am trying to create a form builder using LiveView. Users can build form using drag a drop field types. One of the field types is File. Users can have multiple File type fields in one form. Now I am rendering the form using a form element.
Since I don’t know how many file type fields the user has in the form, I am trying to use a live component for file upload. This live component has its own form.
The problem is this causes nested forms. One parent form and another form for file upload. Sometimes this works. Sometimes the parent form doesn’t submit at all (due to form nesting).
Can anyone suggest how to solve this problem of dynamic file uploads.
Many thanks.

Note: My file uploads are working without any problem. Its just the parent form that doesn’t get submitted due to nesting.

HTML does not allow nested forms, so you’d need to find a way to use just a single form. But if you’re using live uploads the live_file_input doesn’t even need a f form from form_for, so you should be able to wrap that specific one without form anyways.

1 Like

Thanks for the input.
Please help me with the uploads struct. Since the fields are dynamic I cannot have predefined names for the allow_upload function. So maybe I can do this -

      socket
      |> allow_upload(String.to_atom("field_#{id1}"),
        accept: :any,
        external: &presign_entry/2,
        auto_upload: true,
        progress: &handle_progress/3
      )
|> allow_upload(String.to_atom("field_#{id2}"),
        accept: :any,
        external: &presign_entry/2,
        auto_upload: true,
        progress: &handle_progress/3
      )

But then how do I access these on the front end using the @uploads object? This doesn’t work.

    <%= for entry <- @uploads["field_#{id}"].entries do %>
      <div class="mb-4">
        <div class="mt-1">
          <div class="mb-4">
            <div class="text-sm"><%= entry.client_name %></div>
            <div class="flex items-center space-x-2">
              <div class="border border-indigo-100 rounded-full flex h-2 mt-2 flex-1">
                <div class="bg-indigo-600 h-2 rounded-full" style="width: <%= entry.progress %>%;"></div>
              </div>
              <div class="flex-shrink-0">
                <a href="#" class="text-gray-500 hover:text-red-600 text-xs" phx-click="cancel-entry" phx-value-ref="<%= entry.ref %>" phx-target="<%= @myself %>">Cancel</a>
              </div>
            </div>
          </div>
        </div>
      </div>
    <% end %>

You never want to create atoms based on user input. You’ll likely want to keep an additional lookup table for mapping from your user input to the atom names used. The same is the case for changesets as well btw. They also only work with atom field names.

I know I am asking too much, but when you have time, can you please share some code of how to do that. Just take a simple example of “field_1”. How do I access this in the @uploads object. Thanks very much.

I got it. This works.

    <% entries = Map.get(@uploads, String.to_atom("field_1")).entries %>

This still uses String.to_atom. You never want to do that with user supplied strings. Atoms are never garbage collected so this is a DoS attack vector. Use e.g. a map as a lookup table for using a fixed set of atoms only.

Something akin to this, but likely dynamically created:

lookup = %{ "random_field" => :field_1, "another_name" => :field_2}
atom_field_name = Map.fetch!(lookup, string_field_name)

That way you control the amount of atoms being allocated in memory.

2 Likes

You are correct.
I am using String.to_atom only for field ids generated from code. These are not user supplied.
But maintaining a lookup table is a better approach nonetheless.

Thank you so so much.

You can also use String.to_existing_atom(...) to ensure you aren’t exposed to the DoS attack vector @LostKobrakai mentions. It raises an exception if the atom doesn’t already exist.

1 Like

Note that the uploads key can be a string, so no need to cast to atom.

3 Likes

I’m struggling with a similar issue where a user can add multiple attachments each with their own set of other fields in addition to an uploaded file. From what I can tell to uploads key must be an atom. Passing a string throws an error (no function clause).

There’s an is_atom guard on the allow_upload/3 function. Am I missing something?