Is it possible to trigger a live upload from client-side JS?

I’m pretty new to Phoenix, so bear with me here :pray:

I’m integrating the Trix editor (GitHub - basecamp/trix: A rich text editor for everyday writing) into a LiveView. I’ve got the text-editing features working, but now I’m working on file uploads, and I’m wondering if it is possible to use live uploads to make this work.

File uploads in Trix are accomplished by listening for a javascript event emitted by the editor on the client:

Storing Attached Files

Trix automatically accepts files dragged or pasted into an editor and inserts them as attachments in the document. Each attachment is considered pending until you store it remotely and provide Trix with a permanent URL.

To store attachments, listen for the trix-attachment-add event. Upload the attached files with XMLHttpRequest yourself and set the attachment’s URL attribute upon completion. See the attachment example for detailed information.

Is it possible to initiate a live upload from client-side javascript like this? If so, can anyone point me in the right direction on how to get started?

Much appreciated :pray:

phoenix hooks expose a this.upload/uploadTo function to do exactly that JavaScript interoperability — Phoenix LiveView v0.20.2

  • upload(name, files) - method to inject a list of file-like objects into an uploader.
  • uploadTo(selectorOrTarget, name, files) - method to inject a list of file-like objects into an uploader.
    The hook will send the files to the uploader with name defined by allow_upload/3
    on the server-side. Dispatching new uploads triggers an input change event which will be sent to the
    LiveComponent or LiveView the selectorOrTarget is defined in, where its value can be either a query selector or an
    actual DOM element. If the query selector returns more than one live file input, an error will be logged.
4 Likes

Oh bummer, I must have missed that when I looked through the docs. Thanks!

Apologies for resurrecting an old thread, I’m just trying to use Trix + Phoenix LiveView myself. Did you get to a working solution with upload()?

I’ve got as far as this:

Custom input component that has the phx-hook="Trix" and contains a hidden live_file_input component to capture files

  def input(%{type: "trix"} = assigns) do
    ~H"""
    <div phx-feedback-for={@name} phx-hook="Trix" phx-debounce="blur" id={"#{@id}-container"}>
      <.label for={@id}><%= @label %></.label>
      <input
        type="hidden"
        name={@name}
        id={@id}
        value={Phoenix.HTML.Form.normalize_value(@type, @value)}
        {@rest}
      />
      <.live_file_input upload={@upload} class="hidden" />
      <div id="trix-editor-container" phx-update="ignore">
        <trix-editor class="trix-editor" input={@id}></trix-editor>
      </div>
      <.error :for={msg <- @errors}><%= msg %></.error>
    </div>
    """
  end

In JS, callback for uploading files (omitted some of Trix event listeners:

uploadFileAttachment(attachment) {
    const { file } = attachment;
    const input = document.querySelector("input[name='attachment']");

    const dataTransfer = new DataTransfer();
    dataTransfer.items.add(file);
    input.files = dataTransfer.files;

    // Trigger the LiveView file upload
    const upload = this.upload("attachment", [file]);
    attachment.setAttributes({ url: "https://via.placeholder.com/150" });
  },

This correctly sends data to the component and I get it all here:

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

  defp handle_progress(:attachment, entry, socket) do
    if entry.done? do
      uploaded_file =
        consume_uploaded_entry(socket, entry, fn %{path: path} = meta ->
          dest = Path.join("priv/static/uploads", Path.basename(path))
          File.cp!(path, dest)
          # Todo: figure out public URL
          {:ok, dest}
        end)

      socket = assign(socket, uploaded_files: [uploaded_file | socket.assigns.uploaded_files])

      {:noreply, put_flash(socket, :info, "file uploaded")}
    else
      {:noreply, socket}
    end
  end

Ignoring the fact that dest isn’t an actual URL to the file, even if I generate one, I don’t think I can pass it back to the JS – attachment.setAttributes({ url: "https://via.placeholder.com/150" }); expects a “saved” URL that’s permanent.

I’m now leaning towards a standalone controller to handle uploads as described in here How to handle file upload using Trix editor in a Phoenix application but I wondered if I’m missing something.

What do you mean by “I don’t think I can pass it back to the JS”? You could have your handle_progress/3 return a :reply instead of :noreply that contains the url you want? You could also push_event/3 and listen for that event on the client side — I’m not sure I understand what issue you’re having.

I used another method to do similar to the example provided by the trix documentation and the link you included at the bottom of your post and use a controller that then implements the uploader and upload logic.

# in MyAppWeb.TrixUploadsController
  def create(conn, params) do
    case impl().upload(conn.private.plug_session, params) do
      {:ok, file_url} -> send_resp(conn, 201, file_url)
      {:error, _reason} -> send_resp(conn, 400, "Unable to upload file, please try again later.")
    end
  end

  defp impl, do: Application.get_env(:my_app, :uploader)[:adapter]

Similary, removing attachments in the trix-editor calls delete/2:

# in MyAppWeb.TrixUploadsController
  def delete(conn, %{"key" => key, "content_type" => content_type}) do
    case impl().delete_file(key, content_type) do
      :ok -> send_resp(conn, 204, "File successfully deleted")
      {:error, _reason} -> send_resp(conn, 400, "Unable to delete file, please try again later.")
    end
  end

In the uploader you can implement processing the file as well as sending the request. In our case for Mosslet, we check the image for safety, presign urls, and encrypt the image before sending it to cloud storage.

This works well and then I implement more checks in the trix.js to communicate with the server to handle undo/redo of attachments and ensuring the form isn’t submitted before the attachments are uploaded to the cloud and urls are ready to be saved to the database.