How to replace render_existing/3 with function_exported?/3

I have several apps with something like the following in the app.html.eex or app.html.heex:

<%= render_existing(view_module(@conn), "_meta." <> view_template(@conn), assigns)
  || render AppWeb.LayoutView, "_meta.html", assigns %>

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

1 Like

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.

1 Like

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.

2 Likes

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.

Oh, not at all! Your comment was fine.

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?

I’m curious.

2 Likes

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.

You’d replace your code with something like this:

<%= if function_exported?(view_module(@conn), :meta 1), do: view_module(@conn).meta(Map.merge(assigns, %{template: view_template(@conn)}), else: meta(assigns) %>
# LayoutView
def meta(assigns) do
  ~H"""
  …
  """
end

# OtherView
def meta(%{template: "index.html"} = assigns) do
  ~H"""
  …
  """
end

…

From those meta functions you can also call your existing templates using:

render("_meta.#{assigns.template}", assigns)
3 Likes

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

1 Like

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

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) %>
1 Like

FWIW - I just hit the same wall :frowning:

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.

1 Like