File Download while in liveview

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.

2 Likes

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.

2 Likes

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?

2 Likes

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.