Written using Phoenix 0.13.1
One common task in web apps is setting a custom page title for each URL - your homepage should have a different title than a page listing out all of your projects (for example), which should have a different title than a page showing details of a single project.
There isn’t a built-in way to do this in Phoenix, but after much reading around forums, GitHub issues, etc. I hit on two potential ways to do it.
Both involve using a custom page title call in
<!DOCTYPE html> <html lang="en"> <head> <title><%= page_title(@conn, assigns) %></title> ...
Both methods also require you to make the
action_name/1 controller helper available to your views. You can do this in
web/web.ex by adding the
action_name method to the following line:
defmodule Ticketee.Web do ... def view do quote do ... # Import convenience functions from controllers import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1, action_name: 1] ... end end ... end
But the two methods differ in how I’ve defined that
Method 1 -
This leads to a nicer interface for views, at the expense of a hacky
catch block in the
LayoutView, which is used by
application.html.eex, define the following methods:
defmodule Ticketee.LayoutView do use Ticketee.Web, :view def page_title(conn, assigns) do try do apply(view_module(conn), :page_title, [action_name(conn), assigns]) rescue UndefinedFunctionError -> default_page_title(conn, assigns) end end def default_page_title(_conn, _assigns) do "Ticketee" end end
This will call the
page_title/2 method in the view module used by the specific page template - eg. for
ProjectController.index/2, it would use
ProjectView. It passes the
page_title/2 method the name of the current action, and the
assigns data, in case you need it for generating the page title.
In the view modules, you can then define some really simple methods to set the page titles:
defmodule Ticketee.ProjectView do use Ticketee.Web, :view def page_title(:show, assigns), do: assigns[:project].title def page_title(_, _), do: "Projects" end
Alternatively, if you don’t like the pattern-matching style, you could have a single
page_title/2 method and use a
case statement inside it.
I think this is a really elegant solution, that gives a really nice interface where it matters the most - in the individual view modules. The downside is, of course, that hacky
rescue needed to provide a fallback page title, in case the view module doesn’t define the
page_title/2 method. If there’s a nicer way to do that, I’m all ears! Please leave a comment below!
This method leverages the
render_existing method provided by Phoenix, which replicates the
content_for method familiar to those of us coming from Rails. You can use it to render templates specific to a template or controller, or as I’ve used it here, pattern-match a method call.
LayoutView, you can define the following
defmodule Ticketee.LayoutView do use Ticketee.Web, :view def page_title(conn, assigns) do render_existing(view_module(conn), "page_title", Dict.put(assigns, :action_name, action_name(conn))) || default_page_title(conn, assigns) end def default_page_title(_conn, _assigns) do "Ticketee" end end
render/2 in the relevant view module, passing the first argument of “page_title”, and the second argument of the
assigns data, combined with the current action name.
In the view module, you can then pattern-match the
render/2 call with the following method:
defmodule Ticketee.ProjectView do use Ticketee.Web, :view def render("page_title", assigns) do case assigns[:action_name] do :show -> assigns[:project].title _ -> "Projects" end end end
If the view module doesn’t define the
render("page_title", _) method, then Phoenix will fall back to looking for a template with the name
page_title, and if there isn’t one defined,
render_existing/3 will just return nil, meaning the fallback will be used. It’s likely a more “Phoenix-y” way to do it, using Phoenix’s built-in tools, but I think the interface it provides is nowhere near as nice.
I’m only learning both Elixir and Phoenix, and undoubtedly there will be better ways to implement what I have described above. I think I’ll be moving forward with the first solution in my practice Ticketee app, while still looking for a nicer solution to get rid of that
Side note: Yes, I am rebuilding Ticketee from Rails 4 in Action as practice - you can see my progress here!