Whenever you use Phoenix's generators to create a full HTML scaffold, you'll see that in the template directory, there's a form.html.eex
"partial" or sub-template that's used inside of both the new.html.eex
and the edit.html.eex
. This makes perfect sense since the form to edit a new user (or article or comment or whatever) generally takes the same attributes as the form used to create it.
The entire form is embedded inside those new
and action
templates with just this single line:
<%= render "form.html", Map.put(assigns, :action, Routes.article_path(@conn, :create)) %>
You can use the same pattern to embed any eex template into another in the same directory. So far, so good. There's no reason we'd want to embed a user form inside an article template or visa versa. The vast majority of the partial templates you create will only be used by other templates in the same directory and therefore using the same view, the same controller and the same schema.
What about templates from other views?
But what if you want to share a template across controllers? What if you have something like an email signup form that you want to present on blog posts, on sales pages and on many other parts of your application?
Fortunately, Phoenix.View
includes a render/3 function that will do the task for us. Its take three arguments:
- a View module
- a Template under that uses that View
- assigns (just like we're used to passing from a Controller)
Therefore, if we wanted to embed our user form inside of a non-user template, we could write the following in a template:
<%= render(MyAppWeb.UserView, "form.index.html", assigns) %>
Where to put shared templates?
If a template truly is shared across your entire app, it makes sense to create a new view for shared templates instead of just dropping it into the same directory with user or some other type of template.
So let's create a new file at my_app_web/views/
and call it shared_view.ex
. It will be simple, just like nearly all our views:
defmodule MyAppWeb.SharedView do
use MyAppWeb, :view
end
Now, we can create a new directory inside my_app_web/templates
and call it shared
(to match the name of SharedView
). We can make a new template called mail_signup.html.eex
to inside my_app_web/templates/shared
and create our email signup form there.
We can then embed that form into other templates with the following:
<%= render(MyAppWeb.SharedView, template, assigns) %>
Making it more convenient
If you're using a lot of shared templates, the above can be a bit verbose. One way to fix this is by making a render_shared
function available in every template. We can do that from my_app_web.ex
. We'll add the new function to the end of the view
section:
def view do
quote do
use Phoenix.View,
root: "lib/my_app_web/templates",
namespace: MyAppWeb
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
import MyAppWeb.Router.Helpers
import MyAppWeb.ErrorHelpers
import MyAppWeb.Gettext
def render_shared(template, assigns \\ []) do
render(MyAppWeb.SharedView, template, assigns)
end
end
end
Now every view and therefore every template has access to render_shared
. The new, shorter syntax to render the email signup form inside any template in our app is now:
<%= render_shared "mail_signup.html" %>
Any other partial template snippets we put in my_app_web/templates/shared
will be similarly accessible from any template in our app!
Note: we defined the render_shared
function inline in my_app_web.ex
instead of in shared_view.ex
because otherwise SharedView would be importing itself when expanding the macro at the top of its module.
Another approach we could use to eliminate the inline function would be to put the render_shared
function into another module that isn't a view and then import that module into the MyAppWeb module as its generated module doc suggests. It's a bit difficult to think of a non-view module that would be a clearer place to hold the function, however.
I'm more than open to feedback and suggestions on structuring this!