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
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?
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
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
def view do
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
# Use all HTML functionality (forms, tags, etc)
def render_shared(template, assigns \\ ) do
render(MyAppWeb.SharedView, template, assigns)
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!