This setup renders the metadata for a given view “foo.html” by looking for “_meta.foo.html” and rendering it if it exists. I use the meta templates for holding SEO-related meta, OG meta, and various other tags that go into the tag of a document.
If meta doesn’t exist, then the logic above renders a default template called layout/_meta.html. as the default source.
After upgrading Phoenix, I’m now seeing a compilation warning telling me to use function_exported?/3 instead of render_existing/3.
warning: Phoenix.View.render_existing/3 is deprecated. Use function_exported?/3 instead
lib/app_web/templates/layout/app.html.eex:2: AppWeb.LayoutView."app.html"/1
What would be a good way of following this advice (without breaking the behavior of 100+ _meta templates)? There’s a considerable gap in the interface of the suggested replacement function.
Perhaps one approach would be to define a global view helper render_meta/2
def render_meta(conn, assigns) do
try do
render(view_module(conn), "_meta." <> view_template(conn), assigns)
rescue
Phoenix.Template.UndefinedError ->
render(AppWeb.LayoutView, "_meta.html", assigns)
end
end
and call it in your templates with render_meta(@conn, assigns)
Also I’m not 100% sure, but I think you can remove the module name view_module(@conn) from your render call when you’re referencing the view that you’re calling from.
EDIT: There are some examples on using function_exported?/3 in the docs if you want to follow the compiler warning’s suggestion, but seems like it might require you to rewrite your meta templates into functions
Of course it will do it. I’m very tempted to implement something similar for some static templates I have for several locales: render_existing_locale/2. Each template is suffixed with a locale.
Exactly that is the conclusion I also came to after trying that.
By the way could someone, please, elaborate a bit on the story behind this deprecation ?
Knowing it would prevent some people from reproducing by other means that very mistake that the Phoenix team want to discourage.
I looked at the docs before creating this thread. It’s because the examples provided weren’t applicable to my use case that I asked here in the first place.
I’ve found that _meta.foo.html files for simpler cases and defining a render("_meta.foo.html", assigns) functions in the view for more complex cases a very useful pattern.
When I was talking about certain people I meant of course less experienced developers like myself. I realize now that you might misinterpret it. I apologize if you felt targeted.
I was just clarifying that I had already looked through the suggestions and hadn’t seen a way to apply them to this use case.
I also wonder the exact same thing you asked. Was there a usage the team was trying to prevent by deprecating the function? Or was it maybe for some more general optimization in Phoenix?
The idea is moving to function components over templates. Function components are a lot more flexible than templates and given they’re based on (differently named) functions there’s less phoenix magic needed – magic the phoenix teams seems to like to get away from. But not everybody will jump on board of restructing their projects immediately, therefore I feel the deprecation was a bit premature.
Hmm… this will mean making a new function for every single template, which is enough that I’d rather not upgrade some of my apps.
It’s a really useful idea, though. It feels like it’s close. I’ll mess around with this and see if I can come up with something that fixes the issue without writing a hundred new functions. Thank you so much for the help!
I feel a lot more motivated to tackle this upgrade again. I’ll share results when I come up with something.
The trickiest part is how to programatically determine if that template exists and is safe to call. E.g., in many cases, _meta.show.html.eex exists, but _meta.edit.html.eex does not (since SEO isn’t a concern for that action).
I gave it my best shot this morning during a livestream before work and unfortunately, just couldn’t get a working solution.
Without render_existing/3, we can no longer determine whether a template exists. Depending on deployment strategies, the filesystem is sometimes no longer available.
Unfortunately, function_exported?/3 doesn’t fill the gap since render/3 will always be exported.
This means the only options would be
writing and maintaining hundreds and hundreds of meta functions throughout the application
crawling the filesystem at compile time to create a list of all existing template names
using try and rescue on every page load
some kind of macro wizardry I haven’t yet been able to think of
I’ve also not been able to find an adequate solution without using one of the options you listed, and went with the route I look for layout files at compile time.
This is how I render additional actions (buttons) in the navbar, if the view defines a layout for the current action. I hope it’s easy enough to adjust for your needs and save you some time.
action_view.ex
defmodule MyAppWeb.ActionView do
@moduledoc """
Adds a `actions/2` function for all Views that define at least one
template that matches the pattern "*_actions". `actions/2` accepts
the current template (e.g. "show.html"), checks if the actions-template
(e.g. "show_actions.html") has been defined and renders it if so, otherwise it will return nil.
Using this fucntion in a Module that does not `use` Phoenix.View first will fail.
"""
defmacro __using__(_) do
quote do
@before_compile unquote(__MODULE__)
end
end
# Code extracted from
# https://github.com/phoenixframework/phoenix_view/blob/v1.1.2/lib/phoenix/template.ex#L190
defmacro __before_compile__(env) do
root = Module.get_attribute(env.module, :phoenix_root)
pattern = Module.get_attribute(env.module, :phoenix_pattern)
engines = Module.get_attribute(env.module, :phoenix_template_engines)
paths = find_actions(root, pattern, engines)
if Enum.any?(paths) do
clauses =
Enum.map(paths, fn path ->
action_template = Phoenix.Template.template_path_to_name(path, root)
template = String.replace(action_template, "_actions", "")
quote do
def actions(unquote(template), assigns), do: render(unquote(action_template), assigns)
end
end)
quote do
unquote(clauses)
def actions(_, _), do: nil
end
end
end
defp find_actions(root, pattern, engines) do
extensions = engines |> Map.keys() |> Enum.join(",")
root
|> Path.join(pattern <> "_actions*.{#{extensions}}")
|> Path.wildcard()
end
end
You can use MyAppWeb.ActionView on views that provide additional actions or in view/0 in my_app_web.ex and call it in your HTML files like below.
<%= if function_exported?(view_module(@conn), :actions, 2),
do: view_module(@conn).actions(view_template(@conn), assigns) %>
This looks like the least bad of the options. But still - the smelly feeling remains. Especially that it shouldn’t be necessary in the first place, eh.