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.17.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.

7 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

2 Likes

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
4 Likes

If someone is coming here to do it the hard way (without embed_templates, eg you need more control).
This changed with LV 0.18, one now has to:

EEx.function_from_file(
          :def,
          :fun_name,
          source,
          [:assigns],
          engine: Phoenix.LiveView.Engine,
          caller: __ENV__,
          source: source
)
4 Likes

warning - not true, this is leex engine.
Cant get this to work with heex-engine. Using Phoenix.LiveView.HTMLEngine will complain that it has no init/1.

So I now do Phoenix.Template.compile_all which is a little odd if you want to get a function for exactly one file. But you can still do it:

if you have this template:

/foo/bar/template.html.heex

Phoenix.Template.compile_all(
  fn _ -> "template" end,
  "/foo/bar/",
  "template.html" # do not add .heex here! Its automatically added to the pattern!
)

note, that you can just give this to the engines arg:

%{md: Phoenix.Template.engines().heex, ...}

Now you can read markdown into a template! Cool.

its me again :crazy_face:

Got heex 0.18 running with EEx.function_from_file.

EEx.function_from_file(
    :def,
    :sample,
    "sample.html.heex",
    [:assigns],
    engine: Phoenix.LiveView.TagEngine,
    caller: __ENV__,
    source: "sample.html.heex",
    tag_handler: Phoenix.LiveView.HTMLEngine
  )

Dont ask me whats going on, no idea, (and tag_handler is also not yet documented) I copied the solution from here: Using EEx.eval_string with HEEx Engine on live_view 0.18.18 - #2 by chrismccord