Yielding to content in Phoenix templates

The reason

So, there is something missing in Phoenix templating that I've had in other templating systems. Something where I can define a place where I can yield to content from a child template.

After some googling, I haven't found a solution that I like, although I think this one is starting on the right path. I'm gonna run with that idea, but change it up. I don't want to make a specific method for each piece of dynamic content I want to insert, and I certainly don't want one for each default either.

I'm going to walk you through my solution for this.

First make a helper

web/views/layout_helpers.ex:

defmodule MyApp.LayoutHelpers do
  import Phoenix.View, only: [render_existing: 3]
  import Phoenix.Controller, only: [view_module: 1, action_name: 1]
  
  def yield(name, assigns, default \\ "") do
    assigns = Map.put(assigns, :action_name, action_name(assigns.conn))
    render_existing(view_module(assigns.conn), name, assigns) || default
  end
end

and then add it to the imports in web/web.ex in the view method:

  def view do
    quote do
      use Phoenix.View, root: "web/templates"

      # Import convenience functions from controllers
      import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]

      # Use all HTML functionality (forms, tags, etc)
      use Phoenix.HTML

      import MyApp.Router.Helpers
      import MyApp.ErrorHelpers
      import MyApp.Gettext
      import MyApp.LayoutHelpers # Right here!
    end
  end

Using it

In a layout template file, you can now add it anywhere. An example of page-specific title tags, with a fallback.

<title><%= yield("title", assigns, "My App") %></title>

Then in a child view, you can add a render method that will catch it, like so:

defmodule MyApp.PostView do
  use MyApp.Web, :view
  
  def render("title", %{post: post}), do: post.title
end

And then the page will show the post's title.

If you want to do different things based on action_name, you can do:

defmodule MyApp.PostView do
  use MyApp.Web, :view
  
  def render("title", assigns = %{action_name: action_name}) do
    case action_name do
      :show -> assigns[:post][:title]
      :index -> "All the posts!"
    end
  end
end

That's basically all there is to it!

Simple, effective.