Workaround for Liveview disconnecting after submitting form action over HTTP

I have this situation :slightly_smiling_face:

I’m using a liveview to show a list of auctions (as a table) and a button on each row to export each auction as a PDF.
BTW, the PDF is prepared through weasyprint (following the advice of this post).

So, I’m using a mini-form (with ‘GET’ method) in each row in order to utilise the phx-trigger-action binding and submit the auction_id to the controller, in order for the action to prepare the pdf and send it to the user as a download.
The form is like :

<form method="get" for={:pdf} id={"pdf-form-#{auction.auction_id}"} 
action={~p"/download/auction/#{auction.auction_id}"}
phx-trigger-action={@pdf_auction == auction.auction_id} hidden />

All things are working as expected :
The action in the controller is fired and the PDF is prepared
Then the PDF is send as a download to the user browser

Except… after the form submission the liveview disconnects. The liveview page does not refresh, nor redirect. It’s just not interactive any more.
I’ve read the respective docs and I understand, this is actually an expected behaviour.

Once phx-trigger-action is true, LiveView disconnects and then submits the form.

Is there a way to continue in connected state?
Do I have to re-mount the liveview inside the controller action and how?
Is my approach of preparing the PDFs and sending to the user entirely wrong?

Instead of a GET form you can use a link with target="_blank"

2 Likes

Thanks for the suggestion.
I’ve tried that.
It works but the user experience it’s not optimal and not the same in every browser.
In Safari, It opens a blank tab and focuses in this.
In Chrome it opens the tab and then quickly closes it.

Thanks though, this is the best so far.
I thought that maybe there was another way to keep LiveView connected.
Do you think it’s an edge case for wanting the LV to stay connected after phx-trigger-action? Should I open an issue in GH?

Not sure this will help much. By my understanding that’s just how browsers are meant to work. You’re navigating away from the current page so the JS context is stopped. Only later the browser will be notified that the returned data is not a html page, but some other data meant to be downloaded to disk.

You can also look into doing the download from within iframes, which will also leave the outer context unaffected.

2 Likes

You’re navigating away from the current page so the JS context is stopped

Actually, I don’t agree with that sentence in this specific situation.
My intention is not to “navigate away”.
It’s to initiate a process of preparing the PDF is send it to the user.

By this logic, the link with target=“_blank” solution that you suggested, can be also considered as a navigation link. But the LiveView process continues to stay connected, in that case!

Setting target="_blank" tells the browser to open a new context (tab or window) to open the page in instead of using the current context. That‘s why the current context isn‘t halted by the browser.

3 Likes

Hmm, have you tried using buttons with a phx-click binding and a phx-value set to auction_id and then prepare the pdf in the LiveView’s handle_event callback?

Also, this forum post seems relevant to your use case:

1 Like

From where I stand you need an asynchronous request thus xhr or a ws requests.
So basically as @codeanpeace says use LiveView or some other JS to hijack the request of the form before the form submits if you must keep the form’s action request.

User clicks submit, JS hijacks the event & initiates whatever, form goes along its business. :person_shrugging:

I guess what I don’t understand is why the request must go through the controller as a http request.
I feel like you should still be able to trigger a download async or maybe I’m wrong.

Also on the issue of scale, it’s sometimes better to back end these types of requests and email said a thing or provide a link.

As others have pointed out, I think you’re better off creating your PDF as a response to a normal click event in your LV. It’s also a good idea to do it in a separate process (such as a task) because if you block the LV process for too long (around 30 secs) the client will reconnect.

Then you can render a normal download link to download the file. You can use target = "_blank" and the Content-Disposition: attachment header (Content-Disposition - HTTP | MDN) when sending the file. The header tells the browser it should download the file and not display it in the page.

EDIT: probably simpler than the header, you can set the download attribute in the <a> tag to tell the browser it’s a file meant to be downloaded

1 Like
defmodule MedimanWeb.BackupController do
  use MedimanWeb, :controller

  def download(conn, _params) do
    path = System.fetch_env!("DATABASE_PATH")
    send_download(conn, {:file, path})
  end
end

Link to “/database/download” with target=“_blank” works wonders.

1 Like

Thank you all for your suggestions!
@codeanpeace pointed to an interesting thread about a similar case and general handling of downloads.

I’ll try @evadne suggested solution in that thread and see if I have better results than rendering to a link with target=“_blank”.

Thanks @BartOtten, I’m using the version of send_download where the file is not stored : send_download(conn, {:binary, contents}, filename: "Filename.pdf")

1 Like

Literally saved me from insanity, thank you