Adding PDF generate feature in Phoenix LiveView app

Adding PDF generate feature in Phoenix LiveView app

ยท

6 min read

Generating pdf is very common in day-to-day web applications. In E-commerce websites like Amazon when we do any shopping we get the option to get the invoice. It's not an easy task to implement it people often have a hard time implementing it because of different dependencies issues. You can implement it easily with Phoenix LiveView because of the awesome Elixir ecosystem. In this blog, I am going to explain every step that is required to implement the feature to generate and download PDF. So, let's get started.

Creating a Phoenix LiveView Project

First of all, we should have a LiveView project. If you have one skip this section and continue Installing Chromic PDF section. If you don't have one then create a new LiveView project by running mix phx.new my_app --live. We will then navigate to the project directory by running cd my_app in the terminal. And finally, run the project running mix phx.server in the terminal. Once, the project starts running navigate to localhost:4000 to see the "Welcome phoenix" page.

Installing Chromic PDF

To create a PDF, the Elixir community has an awesome library called chromic_pdf. It is very easy to integrate. It does not use puppeteer, and hence does not require Node.js. It communicates directly with Chrome's DevTools API over pipes, offering the same performance as puppeteer, if not better. So we are going to use it. Let's add it to our project. Open the mix.exs file and make an entry of it as shown below.

defp deps do
  ...
  {:chromic_pdf, "~> 1.14"},
  ...
end

Then install the dependency by running mix deps.get. It will install all the dependencies. Now, we will run ChromicPDF as part of our application by adding it in application.ex.

defmodule MyApp.Application do
   ...
  def start(_type, _args) do
    children = [
      ...
      ChromicPDF
      ...
    ]
    ...
  end
...
end

This will make sure that this process should be started when the application starts and is supervised by the Supervisor.

Adding endpoint to generate PDF

We want our pdf generator to be served at a different endpoint and serve a regular HTTP response because we cannot send pdf over a web socket i.e. LiveView. We will add a GET endpoint /generate-invoice under BillsController.

scope "/", MyAppWeb do
    pipe_through [:browser, :require_authenticated_distributor]

    get "/generate-invoice", BillsController, :index
end

Make sure to add the require_authenticated_distributor plug (or any other authentication/authorization plug) in the pipeline to make sure the endpoint is authenticated else it will be exposed to the attackers.

Adding Controller

Now, we will add the controller for the endpoint we added in the router

defmodule MyAppWeb.BillsController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    #Assign some attributes and values which we can use in the pdf.
    conn = assign(conn, :total_amount_paid, 10000) 
    {:ok, pdf} = to_pdf(conn.assigns) ### We will write this function later.

    send_download(
        conn,
        {:binary, Base.decode64!(pdf)},
        content_type: "application/pdf",
        filename: "invoice"
    )
  end
end

At the end of the index action, we can see the send_download button which takes 4 parameters:

  • conn: a connection struct.
  • content in binary tuple format i.e.pdf in this case.
  • content_type: the content type.
  • filename: give the file name of our choice.

send_download is used to send the given file or binary as a download. In other words, it will be the reason to download the generated PDF in your browser.

Implementing to_pdf function

In the index action, we saw the to_pdf function in the previous section. We are going to implement it in this section. In this function, we will see ChromicPDF in action. You will see two functions source_and_options and print_to_pdf.

  • source_and_options: The module Template in ChromicPDf usage is mainly to generate HTML from the provided template and styling. We can see it calls the source_and_options function which returns the source and options for a PDF to be printed, as per the given set of template options like content and size of the PDF.
  • print_to_pdf: As the name suggests it Prints a PDF. It is a blocking call which means it will block further code until the PDF is generated.

Refer to the following code

def to_pdf(assigns) do
    [
      content: content(assigns), # `content` function yet to be implemented
      size: :a4,
    ]
    |> ChromicPDF.Template.source_and_options()
    |> ChromicPDF.print_to_pdf()
end

Generating content for the PDF

Now, the most important part of the implementation. You have seen the content function in to_pdf which is yet to be implemented. We are going to implement that and learn why we are going to use components and not views. Earlier, Phoenix.View was used to render HTML (view) which was later passed to the ChromicPDF#source_and_options function. But since Phoenix 1.7 the Phoenix.View has been eliminated and substituted with the new Phoenix.Template. Phoenix 1.7 supports function components to render both controller-based and LiveView-based components. So, we are going to use Phoenix.LiveComponent instead of Phoenix.View. For this, we will create a component PdfRenderer component inside the components folder, and in the module, we will add a render function. In this function we will write the HTML and CSS we want to render for the PDF. Refer to the following code.

defmodule PdfRendererComponent do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~H"""
      <h1>Invoice</h1>
      <p style="font-size: 200%;">Total:  <%= @total_amount_paid %> </p>
    """
  end
end

In the above render function you can see the field total_amount_paid attribute. Where does this come from? If you remember in the controller we assigned the total_amount_paid attribute to the conn struct. We are getting it here because of the assign parameter in the render function. To access it in this component we have to pass the conn.assign struct directly to the component. Something like this,

PdfRendererComponent.render(assigns)

We will implement this function content to render this component. Refer to the following code.

defp content(assigns) do
    Phoenix.HTML.Safe.to_iodata(PdfRendererComponent.render(assigns))
end

In Phoenix, you can call the functions of the components using Phoenix.HTML.Safe.to_iodata. The official documentation says "(It) provides better performance when sending or streaming data to the client". So, in this case, the HTML is sent to the ChromicPDF.Template.source_and_options() to render. Whatever HTML and CSS we write in the render function will be passed to the source_and_options. The final working code of the controller is below.

defmodule MyAppWeb.BillsController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    #Assign some attributes and values which we can use in the pdf.
    conn = assign(conn, :total_amount_paid, 10000) 
    {:ok, pdf} = to_pdf(conn.assigns) ### We will write this function later.

    send_download(
        conn,
        {:binary, Base.decode64!(pdf)},
        content_type: "application/pdf",
        filename: "invoice"
    )
  end

  def to_pdf(assigns) do
    [
      content: content(assigns), 
      size: :a4,
    ]
    |> ChromicPDF.Template.source_and_options()
    |> ChromicPDF.print_to_pdf()
 end

 defp content(assigns) do
    Phoenix.HTML.Safe.to_iodata(PdfRendererComponent.render(assigns))
 end
end

Adding Download button

Finally, we need to add an Invoice/Download button to call this endpoint we created. It will be a simple link tag.

<%= link "Invoice", to: "/generate-invoice", class: "button", download: 'invoice.pdf` %>

You can notice I have added a download attribute to the link tag. I wrote a small TIL blog on its usage you can check it later ๐Ÿ˜Š.

I hope you like this blog. If you have any questions please comment below. Thanks for reading ๐Ÿ˜Š.

References

Did you find this article valuable?

Support AbulAsar S. by becoming a sponsor. Any amount is appreciated!