Controller: Implicit rendering with heex

LiveView has spoiled me and now I would LOVE to be able to write controllers like this:

defmodule DeadViewWeb.ItemController do
  use DeadViewWeb, :controller
  alias DeadView.Inventory

  def new(conn, _) do
    conn = conn
      |> assign(changeset:  Inventory.change_item(%Inventory.Item{}))

   ~H"""
    <main>
      <.header>New Item</.header>
      <.item_form changeset={@changeset} action={~p"/items"} />
    </main>
   """ 
  end

  defp item_form(assigns) do
    ~H"""
    <.simple_form :let={f} for={@changeset} action={@action}>
      <.error :if={@changeset.action}>
        Oops, something went wrong! Please check the errors below.
      </.error>
      <.input field={f[:name]} type="text" label="Name" />
      <:actions>
        <.button>Save Item</.button>
      </:actions>
    </.simple_form>
    """
  end
end

I need to migrate a few forms back to controllers, and now the old way feels somewhat clunky, compared to LiveView, where I basically never use .html.heex files. And since controllers don’t need a mount callback with a separate render, this could be even more concise.

The conn = conn |> assign ... part isn’t great, I know - I’m sure there’s a better way.

From a mental model, this would be very similar to the direction remix, waku, Next.js and others in React-land are taking, ie: making it easier to pick up devs that have experience and appreciate these models[1], while also making the transition from Controllers to LiveView (and vice versa), given its similarities with RSC (to some extent).

It feels like something Chris McCord would be able to implement in a short lunchbreak with 20 LOC; so I’m wondering what the problems are with this and whether there’s prior work, before I explore this idea myself.


  1. I count myself somewhat part of that group. ↩︎

This is possible today:

def action(conn, _) do
  assigns = %{…}

  html(conn, Phoenix.HTML.Safe.to_iodata(~H"""
     …
  """))
end
9 Likes

I expected nothing less. :smiley:

Merely skimmed the page on rendering, but now saw that it mentions html/2. When I skimmed it, I was mostly looking at the code examples; and didn’t see any.

TIL! Thanks!

1 Like

I tried this and got the error no function clause matching in Plug.Conn.resp/3.

I think we need something in between html and the ~H"""...""" template to turn the compiled HEEx into iodata as expected by html/2.

defmodule AppWeb.SomeController do
  use AppWeb, :controller
  use Phoenix.Component
  import AppWeb.CoreComponents
  require Phoenix.LiveViewTest

  def action(conn, _params) do
    assigns = %{…}

    html(conn, Phoenix.LiveViewTest.render_component(&page/1, assigns))
  end

  defp page(assigns) do
    ~H"""
    …
    """
  end
end

It does feel weird calling Phoenix.LiveViewTest.render_component, I admit.

Phoenix.HTML.Safe.to_iodata does the conversion.