Render heex templates directly from a Phoenix controller
When creating a controller in a Phoenix application, at least three files are created:
- The controller (e.g. PageController),
- The view (e.g. PageHTML)
- The template (e.g. index.html.heex)
This is a great structure when you’re building CRUD applications.
Sometimes, especially if you have a single action, it would be nice to have a single file for the controller, view, and template.
I tweeted the idea out this morning with a quick example. After thinking about it a bit more, and with some of the feedback, I’ve settled on something to use going forward.
Sometimes all that ControllerHTML module plus templates just seems like overkill. Here’s a way to make a controller with inline templates (like render/1 within a LiveView)#myelixirstatus pic.twitter.com/2WhsqAw3pr
— Andrew Timberlake (@ATimberlake) October 7, 2024
I create a sub-module within the controller named View
which contains the template functions using the heex ~H
sigil. Then the magic is done with put_view(conn, __MODULE__.View)
in the controller (handled at the top of the controller as a plug).
See the code for the contact form from this blog below.
The router
scope "/", AndrewTimberlake.Web do
pipe_through [:browser]
get "/contact", ContactController, :new
post "/contact", ContactController, :create
get "/contact/thanks", ContactController, :thanks
end
The controller
defmodule AndrewTimberlake.Web.ContactController do
use AndrewTimberlake.Web, :controller
import Shorthand
plug :put_view, __MODULE__.View
def new(conn, _params) do
form = to_form(%{}, as: :contact)
conn
|> render(:new, m(form))
end
def create(conn, sm(contact: params)) do
{:ok, _} = AndrewTimberlake.ContactNotifier.deliver_contact_form(params)
conn
|> put_flash(:info, "Message sent")
|> redirect(to: "/contact/thanks")
end
def thanks(conn, _params) do
conn
|> render(:thanks)
end
defmodule View do
use AndrewTimberlake.Web, :html
def new(assigns) do
~H"""
<div class="grid max-w-md gap-2 mx-auto">
<.header>Contact me</.header>
<p>Fill in the form and say đź‘‹.</p>
<.form for={@form} method="post" action={~p"/contact"} class="grid gap-4">
<.input field={@form[:name]} label="Name" />
<.input type="email" field={@form[:email]} label="Email" required />
<.input type="textarea" field={@form[:message]} label="Message" required />
<.button>Send</.button>
</.form>
</div>
"""
end
def thanks(assigns) do
~H"""
<div class="grid max-w-md gap-2 mx-auto">
<.header>Thanks đź‘Ť</.header>
<p>I'll get back to you soon.</p>
</div>
"""
end
end
end
and send me a message to say hi at /contact