Andrew Timberlake Andrew Timberlake

Hi, I’m Andrew, a programmer and entrepreneur from South Africa,
building Mailcast for taking control of your email.
Thanks for visiting and reading.


Render heex templates directly from a Phoenix controller

When creating a controller in a Phoenix application, at least three files are created:

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.

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

7 Oct 2024