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 moduleTemplate
inChromicPDf
usage is mainly to generate HTML from the provided template and styling. We can see it calls thesource_and_options
function which returns the source and options for a PDF to be printed, as per the given set of template options likecontent
andsize
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โll notice I have added an 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 ๐.