AlchemistCamp

AlchemistCamp

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.

First 10 of 11 Posts Switch mode

msimonborg

msimonborg

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

Kurisu

Kurisu

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.

AlchemistCamp

AlchemistCamp OP

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.

Kurisu

Kurisu

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.

AlchemistCamp

AlchemistCamp OP

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.

LostKobrakai

LostKobrakai

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

AlchemistCamp OP

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.

AlchemistCamp

AlchemistCamp OP

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

AlchemistCamp

AlchemistCamp OP

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
Awlexus

Awlexus

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

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
rahultumpala
Hello, I have an Elixir backend that implements a custom protocol over TCP. I want to load test the backend and assess the performance o...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New

We're in Beta

About us Mission Statement