EEx.function_from_file for Heex

with EEx.function_from_file I can create a function from an eex-template, call it and get a rendered string back.

How can I do that with ~H?

You want to use a separate .heex.html file:

  • to define a component
  • use it instead of ~H in a component?

I have not come across an out of box api in phoenix_live_view to achieve the first.
If you are looking for second one - there is no straight forward way to do this.

Couldn’t you just pass HTMLEngine in the options as engine?

1 Like

I want to use ~H as a template engine to create an HTML file (which I can then process with another tool to PDF). I already have these templates working in a liveview-app, but need some of the views as PDF.

I was hoping I’d find an equivalent to EEx — EEx v1.14.0-dev - example:

# sample.html.heex
<MyComponent.greet name={@name} />

# sample.ex
defmodule Sample do
  require HEEx # <--- does not exist
  HEEx.function_from_file(:def, :sample, "sample.html.heex", [:a, :b])
end

# iex
Sample.sample(name: "World")
#=> "Hello, World!"

You mean like

EEx.function_from_file(
  :def, :sample, "sample.eex", [:assigns], engine: Phoenix.LiveView.HTMLEngine)

this returns a %Phoenix.LiveView.Rendered and it doesn’t look like I’ll get a HTML from that easily.

You can pipe the %Phoenix.LiveView.Rendered into
Phoenix.HTML.Safe.to_iodata() |> to_string()
which would return the html resulting string.

5 Likes

indeed. Some times things are so obvious :melting_face:

defmodule Sample do
  require EEx

  EEx.function_from_file(
    :def, :sample, "sample.html.heex", [:assigns], engine: Phoenix.LiveView.HTMLEngine)
end

defmodule MyComponent do
  use Phoenix.Component

  def greet(assigns) do
    ~H"""
    <p>Hello, <%= assigns.name %></p>
    """
  end
end

# sample.html.heex
<MyComponent.greet name={@name} />
iex(1)> Sample.sample(%{name: "World"}) |> Phoenix.HTML.Safe.to_iodata() |> to_string()
"<p>Hello, World</p>"
7 Likes

this (obviously) needs liveview in deps!

Which is enough though, no other deps needed (Jason if you want LV to stop complaining)

 defp deps do
    [
      {:jason, "~> 1.3.0"}, # recommended by LV
      {:phoenix_live_view, "~> 0.17.10"}
    ]
  end

writing to a file does not need to_string

Sample.sample(%{name: "World"})
|> Phoenix.HTML.Safe.to_iodata()
|> then(fn data -> File.write!(path, data) end)
1 Like

This does not work with LV 0.18, any ideas what to do?

== Compilation error in file lib/sample.ex ==
** (KeyError) key :caller not found in: [file: "lib/sample.ex", line: 23, engine: Phoenix.LiveView.HTMLEngine]
    (elixir 1.14.2) lib/keyword.ex:595: Keyword.fetch!/2
    (phoenix_live_view 0.18.11) lib/phoenix_live_view/html_engine.ex:163: Phoenix.LiveView.HTMLEngine.init/1
    (eex 1.14.2) lib/eex/compiler.ex:295: EEx.Compiler.compile/2
    lib/sample.ex:23: (module)

maybe related to this issue

try

EEx.function_from_file(
  :def, :sample, "sample.eex", [:assigns], engine: Phoenix.LiveView.HTMLEngine, caller: __ENV__)
1 Like

adding caller: __ENV__ leads to the next error:

== Compilation error in file lib/sample.ex ==
** (KeyError) key :source not found in: [file: ...

I don’t really understand why this is necessary, but this should work:

source = "sample.eex"
EEx.function_from_file(
  :def, :sample, source, [:assigns], engine: Phoenix.LiveView.HTMLEngine, caller: __ENV__, source: source)
1 Like

Yes it works. To summarize what to do to use LV 0.18-HTMLEngine as a template engine:

# mix.exs / deps
[
  {:jason, "~> 1.4.0"},
  {:phoenix_live_view, "~> 0.18"}
]
# config.exs
import Config
# only to make Phoenix happy
config :phoenix, :json_library, Jason
# sample.ex
defmodule Sample do
  require EEx

  source = "sample.html.heex"

  EEx.function_from_file(
    :def,
    :sample,
    source,
    [:assigns],
    engine: Phoenix.LiveView.HTMLEngine,
    caller: __ENV__,
    source: source
  )

  def render(assigns) do
    sample(assigns) |> Phoenix.HTML.Safe.to_iodata() |> to_string()
  end

  def render_to_file(path, assigns) do
   # writing to a file does not need to_string
   sample(assigns)
   |> Phoenix.HTML.Safe.to_iodata()
   |> then(fn data -> File.write!(path, data) end)
  end
end
# my_component.ex
defmodule MyComponent do
  use Phoenix.Component

  def greet(assigns) do
    ~H"""
    <p>Hello, <%= assigns.name %></p>
    """
  end
end
# sample.html.heex
<MyComponent.greet name={@name} />
# iex(1)> Sample.render(%{name: "World"})
# "<p>Hello, World</p>"
1 Like

Any reason not to use embed_templates?
https://hexdocs.pm/phoenix_live_view/0.18.11/Phoenix.Component.html#embed_templates/1

1 Like

Just when I think: “what a mess, someone should write a macro that automatically loads the templates!”

I just didn’t know about that. Very nice, thank you.

So the whole thing looks like:

defmodule Sample do
  use Phoenix.Component

  import MyComponent

  # sample.html.heex is here
  embed_templates("templates/*")

  def render(template, assigns, path \\ nil) do
    rendered = apply(__MODULE__, template, [assigns]) |> Phoenix.HTML.Safe.to_iodata()
    if path, do: File.write!(path, rendered), else: to_string(rendered)
  end
end