I have an app that basically calculates data and puts it out both textually and graphically. It’s something I first built without using Liveview.
Conventionally, to serve the generated data as a downloadable excel file, I would send the conn and some data via a post request to a controller, manipulate the conns resp_content_type
and resp_header
, build the response file in a view’s render/2 function and render the response.
How would I approach that from within a Liveview, where conn isn’t available?
At the moment it is not directly supported in live view. What I do is create a form in live view and post it to the controller. In the controller I handle file download. Since its from a live view the page doesn’t change and the users think that the file was directly downloaded from the live view.
In the controller I handle file download.
[sreyansjain] Would you be able to share some code about how you do this?
My solution from before using liveview is this.
The controller:
def download_excel(conn, %{"id" => id) do
#... stuff to generate filename and parameters for render
conn
|> put_resp_content_type("text/xlsx")
|> put_resp_header("content-disposition", "attachment; filename=#{filename}")
|> render("report.xlsx", params: params)
end
the view:
def render("report.xlsx",params) do
{result, balance} = create_report(params)
create_excel_workbook({result, balance})
|> Elixlsx.write_to_memory("report.xlsx")
|> elem(1)
|> elem(1)
end
But obviously, what I’m doing in the controller must be different because I don’t have a conn in liveview.
You’d still want to use controllers for downloads. Http based downloads is what browsers handle, while liveview is rooted in javascript and I’m not even sure if you can get the file reasonably placed on the users file system from there.
Ok,
I’ve gotten this to work, albeit in a very weird way , and it feels kind of strange that this would work and I don’t know why. Here’s the relevant code.
In my liveview .leex template:
<%= f = form_for :excel, Routes.download_excel_path(@socket, :download_excel, @financial_association), id: "excel_form"%>
<%= hidden_input f, :period, id: "hidden_excel_period"%>
<%= hidden_input f, :start_date, id: "hidden_excel_start_date"%>
<%= hidden_input f, :end_date, id: "hidden_excel_end_date"%>
<%= submit "download excel file", class: "btn btn-secondary excel-button", id: "excel_file"%>
</form>
This routes to the Controller:
use AppWeb, :controller
alias App.Properties
def download_excel(conn, %{"id" => id, "excel" => dates}) do
financial_association = Properties.get_financial_association!(id)
filename = financial_association.accounts
|> List.first
|> Map.get(:community)
|> Map.get(:name)
|> (fn x -> x <> "_" <> (Map.get(dates, "start_date") |> String.replace("/","")) end).()
|> (fn x -> x <> "_" <> (Map.get(dates, "end_date") |> String.replace("/","")) end).()
|> (fn x -> x <> ".xlsx" end).()
conn
|> put_resp_content_type("text/xlsx")
|> put_resp_header("content-disposition", "attachment; filename=#{filename}")
|> render("report.xlsx", %{financial_association: financial_association, dates: dates})
end
end
And here’s where things get weird. Because one would expect this to be the working view:
defmodule AppWeb.Export.FinancialAssociationView do
use AppWeb, :view
import App.Excel
import App.AnnualReports
alias Elixlsx
def render("report.xlsx", %{financial_association: financial_association, dates: dates}) do
%{:result => result, :balance => balance} = create_annual_report(financial_association, dates)
create_excel_workbook({result, balance})
|> Elixlsx.write_to_memory("report.xlsx")
|> elem(1)
|> elem(1)
end
end
But instead, this yields a Phoenix.Template.UndefinedError in the browser, because actually the above code has to be in the AppWeb.LayoutView, with “root.xlsx” as matching filename! When put there, and the Export.FinancialAssociationView just has this:
def render("report.xlsx", _params), do: nil
Everything works. Which is great, but I am absolutely clueless why LayoutView suddenly enters the picture. Anyone with ideas?
adding |> put_root_layout(false)
in the controller can solve it.
yeah! I actually created separate pipeline without layout plug and used it for all download file endpoints.