Easiest way to render a .heex template to an HTML string?

Usually in tests of my views, I have used safe_to_string() to convert a returned :safe tuple to HTML that I can assert on. For example:

test "greets the user" do
  user = %{name: "Kurt"}
  html = MyView.render("some_template.html", user: user) |> Phoenix.HTML.safe_to_string()

  assert html =~ "Hello, Kurt"
end

However, if the template is a .heex file, this no longer works because the return struct is a %Phoenix.LiveView.Rendered{} struct, e.g.:

%Phoenix.LiveView.Rendered{
  dynamic: #Function<0.64041902/1 in PeanutButterWeb.PageView."hello.html"/1>,
  fingerprint: 29760870404026531075951007545402085092,
  root: true,
  static: ["<p>Hello, Kurt</p>"]
}

This is also true, even if I’m rendering from a plain view.

Is there a simple way to convert this struct to plain html?

Thanks :+1:

2 Likes

Okay, here’s what I found after digging around for a while:

test "greets the user" do
  user = %{name: "Kurt"}
  html = MyView.render("some_template.html", user: user)
        |> Phoenix.HTML.Safe.to_iodata()
        |> IO.iodata_to_binary()

  assert html =~ "Hello, Kurt"
end

Will give the returned HTML as string, ready for assertions.

By the way, there are very good docs available about it here: Phoenix.LiveView.Engine — Phoenix LiveView v0.16.3

8 Likes

Just to add: this also works for rendering components to plain HTML, without having to define a separate template / view, for example:

DevApp.ProductComponent.render(%{product: %{id: 1}}) 
|> Phoenix.HTML.Safe.to_iodata() 
|> IO.iodata_to_binary()
6 Likes

what do you mean by: DevApp.ProductComponent

I mean in my case I don’t use any kind of component, so what that part is supposed to be?

I have a partial that I want to send it as a string from my controller json response, do you have any idea?

You might have a look at the solution…

what do you mean?

I meant there is an example with view in the solution…

Now You don’t have view anymore, but normally html files should be the replacement.

but that what I was asking, because we have no views now, what’s the appropriate code, I am new to phx so without seeing any concrete code I couldn’t imagine what it should look like

Something like this…

iex(1)> h = ArenaWeb.PageHTML.home []
%Phoenix.LiveView.Rendered{
  static: ["<h1>Welcome!</h1>\n<p>Hello world!</p>\n\n", "\n<br>\n\n",
   "\n<br>\n\n<i class=\"bi bi-0-circle\"></i>\n<i class=\"bi bi-alarm\"></i>\n<i class=\"bi bi-apple\"></i>\n\n",
   "\n", "\n\n", "\n",
   "\n<div class=\"dropdown\">\n    <button class=\"btn btn-danger dropdown-toggle\" type=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n        Dropdown button\n    </button>\n    <ul class=\"dropdown-menu\">\n        <li><a class=\"dropdown-item\" href=\"#\">Action</a></li>\n        <li><a class=\"dropdown-item\" href=\"#\">Another action</a></li>\n        <li><a class=\"dropdown-item\" href=\"#\">Something else here</a></li>\n    </ul>\n</div>\n\n",
   "\n\n", "\n\n", "\n\n", ""],
  dynamic: #Function<0.126745719/1 in ArenaWeb.PageHTML.home/1>,
  fingerprint: 298082719659021152739576324523550059913,
  root: false,
  caller: :not_available
}
iex(2)> h.static |> Phoenix.HTML.Safe.to_iodata() |> IO.iodata_to_binary()         
"&lt;h1&gt;Welcome!&lt;/h1&gt;\n&lt;p&gt;Hello world!&lt;/p&gt;\n\n\n&lt;br&gt;\n\n\n&lt;br&gt;\n\n&lt;i class=&quot;bi bi-0-circle&quot;&gt;&lt;/i&gt;\n&lt;i class=&quot;bi bi-alarm&quot;&gt;&lt;/i&gt;\n&lt;i class=&quot;bi bi-apple&quot;&gt;&lt;/i&gt;\n\n\n\n\n\n\n&lt;div class=&quot;dropdown&quot;&gt;\n    &lt;button class=&quot;btn btn-danger dropdown-toggle&quot; type=&quot;button&quot; data-bs-toggle=&quot;dropdown&quot; aria-expanded=&quot;false&quot;&gt;\n        Dropdown button\n    &lt;/button&gt;\n    &lt;ul class=&quot;dropdown-menu&quot;&gt;\n        &lt;li&gt;&lt;a class=&quot;dropdown-item&quot; href=&quot;#&quot;&gt;Action&lt;/a&gt;&lt;/li&gt;\n        &lt;li&gt;&lt;a class=&quot;dropdown-item&quot; href=&quot;#&quot;&gt;Another action&lt;/a&gt;&lt;/li&gt;\n        &lt;li&gt;&lt;a class=&quot;dropdown-item&quot; href=&quot;#&quot;&gt;Something else here&lt;/a&gt;&lt;/li&gt;\n    &lt;/ul&gt;\n&lt;/div&gt;\n\n\n\n\n\n\n\n"

Sending html on the wire is not optimal.

I have used this technique in Rails long time ago, with ajax calls…

But now Liveview does the same in more optimized way (data on the wire)

Try to learn Liveview… it’s worth it

Thank you for the suggestion, I know about liveview and I will look at it when I am ready, for now I just have a specific use case to send the html that way

from your output I see that tags are replaced by &lt and &gt how I can only stripes the tags coming from the assigns, as the static tags in the template are already safe

Debug annotations get rendered when I call a function that sends HEEx to Phoenix.HTML.Safe.to_iodata() in a .html.heex file.

Debug annotations do not render when the function is called from an .ex file and assigned to the socket.

You can try

Phoenix.Template.render_to_string(<module_name>, function name in string, "html", assigns)

for example

Phoenix.Template.render_to_string(MyAppWeb.TaskHTML, "show_task", "html", task: task)
1 Like

Nice finding, thanks for sharing!

I found one more usable API that achieves the same, with the advantage of a compilation error in case the template name/function is invalid:

MyModule.my_function_component(%{some: assign_value})
|> Phoenix.LiveViewTest.rendered_to_string()

Phoenix.LiveViewTest.rendered_to_string/1’s implementation is, no surprise, similar to the first solution in this thread:

  def rendered_to_string(rendered) do
    rendered
    |> Phoenix.HTML.html_escape()
    |> Phoenix.HTML.safe_to_string()
  end

Example usage

Passing some HTML code to my template, for example an embed code that the user can copy:

  def mount(%{"id" => id}, _session, socket) do
    socket =
      socket
      |> assign(
        id: id,
        embed_code: embed_code(%{id: id}) |> Phoenix.LiveViewTest.rendered_to_string()
        # embed_code: Phoenix.Template.render_to_string(__MODULE__, "embed_code", "html", %{id: id})
      )

    {:ok, socket}
  end

  attr :id, :string, required: true

  def embed_code(assigns) do
    ~H"""
    ...<%= @id %>...
    """
  end
1 Like

I could not figure out how to skip the annotations when they are globally enabled in dev. Would you have an example of how to skip them?

Having the annotations in dev is not problem since they won’t be there in prod, but your comment got me curious :slight_smile:


In my testing everything rendered off a ~H"""...""" HEEx template in an environment with annotations enabled gets the annotations. IIUC that’s expected because sigil_H/2 is a macro that injects the annotations at compilation time.

It’s the first time I’m looking at this code, so excuse me if I make unsound conclusions.

My reading is that sigil_H/1 calls EEx.compile_string/2 passing Phoenix.LiveView.HTMLEngine. The former will do the annotations based on Application.get_env(:phoenix_live_view, :debug_heex_annotations, false).

https://github.com/search?q=repo%3Aphoenixframework%2Fphoenix_live_view%20debug_heex_annotations&type=code

1 Like