LiveView initiate file upload from JavaScript (e.g. drag&drop)?

Is there a way to trigger the same functionality provided by live_component_upload, but from JavaScript?

The question is about darg & dropping and copy/pasting files into a form.

With drag and drop you end up with (pseudocode):

window.addEventListener('drop', function(e){
   // e.dataTransfer.files contains all dropped file metadata
   // so, for each file:
   const reader = new FileReader();

   reader.onload = (function (theFile) {
      return function (e) {
        console.log(reader.result)
      };
    })(file);

    // you can read in the file as any of the following
    reader.readAsDataURL(file); // reader.result will be data:image/jpeg;base64,/9j/4A...
    reader.readAsBinaryString(file); // binary
    reader.readAsArrayBuffer(file); // etc.
})

Once we have this data, is there a way to trigger sending this data?

Or the only solutions are, basically:

  1. Trigger LiveView to generate a form for these files. Wait until the form is on the page (via a hook?). Assign files to the file inputs in the form. Trigger the upload

  2. Just send an event to LiveView with the file data (will lose any progress info)

?

1 Like

Why not have a form beforehand? Instead of generating it on the fly.

1 Like

I don’t know how many files I’ll have beforehand: I may drag in a bunch at once, or just one. I’m also thinking of having multiple dropzones for files on the page.

However, your suggestion did give me an idea: pre-generate the form(s), have it/them on page (but hidden) and assign files to them as needed (and unhiding the relevant parts). Should work wonders with auto_upload. I’ll have to check this tomorrow :slight_smile:

And here’s a solution. Thanks to @olivermt for steering me in this direction.

Do prerender your live upload form:

In your controller or live component:

  def mount(socket) do
    {:ok,
     socket
     |> assign(:uploaded_files, [])
     |> allow_upload(
          :files,
          accept: ~w(.jpg .jpeg .png),
          max_entries: 10,
          auto_upload: true,
          progress: &handle_progress/3
      )}
  end

(see auto_upload and progress in the docs)

In your live template (this code is straight out of docs):

<%= for entry <- @uploads.files.entries do %>
  <%= entry.client_name %> - <%= entry.progress %>%
<% end %>

<form phx-change="validate" phx-target="<%= @myself %>">
  <%= live_file_input @uploads.files %>
</form>

And then in the code that handles your file drops:

window.addEventListener('drop', function(e){
  var dt = e.dataTransfer;
  var files = dt.files;

  // let's find our file input and populate it with dropped files
  const fileInput = el.querySelector('input[type="file"]');
  fileInput.files = files;
  
  // now we have to trigger a change event on the input so that LiveView correctly picks it up

  // NOTE: When tested in Chrome, Chrome required this. However, the even was triggered automatically
  // in Safari
  var event = document.createEvent("UIEvents");
  event.initUIEvent("change", true, true);
  fileInput.dispatchEvent(event);
}

Voilà

This will work with multiple forms as well.

For my case I made a hook on a div that wraps the upload form. I attached the drag/drop events to that div (to show overlays and whatnot). When the file is dropped, I attach the files and send the event to the form inside that particular div. So now I can have however many file dropzones as I need.

1 Like

Well, in Chrome you have to trigger the event manually. In Safari it gets triggered once you set files on the input.