Rendering html templates in Phoenix 1.7 from a subfolder

I’m trying out Phoenix 1.7, but some things are not working like they used to, for instance rendering a template identified by a disk path, say, render(conn, "car/index.html") which fails with the following error when visiting localhost:4000/car:

My Error

no "car/specs" html template defined for MyApp.ProductHTML

My Setup

## HTML dir structure
product_html/
  • car/specs.html.heex
  • bus/specs.html.heex
  • moto/specs.html/heex
# router.ex
...
    get "/car", ProductController, :car
    get "/bus", ProductController, :bus
    get "/moto", ProductController, :moto
...
# product_controller.ex
defmodule MyApp.ProductController do
  use MyApp, :controller

  def car(conn, _params),
    do: render(conn, "car/specs.html")

  def bus(conn, _params),
   do: render(conn, "bus/specs.html")

  def moto(conn, _params),
    do: render(conn, "moto/specs.html")
end
# product_html.ex
defmodule MyApp.ProductHTML do
  use MyApp, :html

  embed_templates "product_html/*"
end

My Issue

How do I render templates located inside a subfolder in Phoenix 1.7, such as render(conn, "car/specs.html")?

I know one solution could be to embed, say, new files such as car_specs.html, bus_specs.html, then render(conn, :car_specs), etc, but I’m trying to adhere to a certain directory structure, namely product_html/car/*.html.heex, product_html/bus/*.html.heex.

embed_templates uses a “file glob” wildcard pattern match syntax, where * is not sufficient to find files in subdirectories. You’ll need to go deeper, with the ** syntax.

2 Likes

Thank you for linking to the “file glob” docs, however that doesn’t solve my problem since I have three files (within my subfolders) named specs.html.heex, and those would map to a single <.specs /> template.

I’m looking for some mechanism to refer to a template from within the controller’s functions, similar to how Phoenix worked in the past when it was using views, thus when calling a template with the same basename (but different paths) it should work correctly.

Thus,

render(conn, "car/specs.html")
render(conn, "bus/specs.html")

should still work.

I see, I misunderstood.

The best option I can get out of reading the docs would be something like:

embed_templates "product_html/car/*.html.heex", suffix: "_car"
embed_templates "product_html/bus/*.html.heex", suffix: "_bus"

That would allow you to change your controller actions to read like:

render(conn, :specs_car)
2 Likes

It also looks like embed_templates is a fairly trivial macro, so you could always take your own approach based on that.

Something like:

defmodule MyTemplates
  defmacro my_embed_templates(pattern, opts \\ []) do
    quote bind_quoted: [pattern: pattern, opts: opts] do
      Phoenix.Template.compile_all(
        &MyTemplates.__custom_embed_name_generator__(&1),
        Path.expand(opts[:root] || __DIR__, __DIR__),
        pattern
      )
    end
  end

  @doc false
  def __custom_embed_name_generator__(path),
    do:
      path 
        |> Path.rootname
        |> Path.rootname
        |> Path.split()
        |> Enum.take(-2)
        |> Enum.join("_")
        |> String.to_atom

As an example, this would let you do something like:

my_embed_templates("product_html/**/*")
render(conn, :car_specs)
2 Likes

It looks like embedding templates makes them available as functions within the *HTML.ex module, thus two templates cannot share the same name even if they’re along different subpaths, so your solution seems like a clean fix since I don’t have to alter the exiting folder structure.

I guess the old ways are gone in the new Phoenix.

Thanks again!

1 Like

Just to be clear, the old ways still work. It’s just that the new ways are different. you can still use classical views in phoenix, you just can’t necessarily convert those to modern HTML views in an identical way.

2 Likes