How to render a template inside a “web/templates/folder/subfolder”

With Phoenix 1.2 we get the ability to do this but I’m trying it without success. I posted a question here:

I think this is a very good improvement for folder/components organization so I post this question here too.

3 Likes

Easiest way, just make a new view that is module-spaced to the directory you want the files to be. :slight_smile:

I do precisely that with a lot of my re-usable HTML components so I can easily embed them around.

1 Like

What do you mean by module-spaced?
I was trying to use the new :path and :pattern options to allow wildcard template inclusion as well as customized template directory locations, but your suggestion seems to solve that without the need of these.
Thanks!

The ‘name’ of a View module defines the directory path it loads. :slight_smile:

For example I have a helper view so I can embed other eex parts into larger areas:

defmodule MyServer.ComponentsView do
  # Stripped code that does not matter
  use MyServer.Web, :view
end

This defines a view that will load eex from my web/templates/components directory.

You can customize how it loads further by instead of 'use’ing your Web’s :view you can just use Phoenix.View, root: "web/templates", namespace: MyServer and change whatever you want. Or you can go even lower level and call the EEX generator to generate functions in your module. I just make new View’s with appropriate names. :slight_smile:

4 Likes

Could you point to a repo online that uses this technique? I also had a similar issue, but I just share a single SharedView without much customization at this point. So I have multiple a.html.eex, b.html.eex, etc. templates with a single view.

See the pattern option to Phoenix.View, i.e. in your web.ex you can set:

use Phoenix.View, root: "web/templates", pattern: "**/*"

Which would then precompile all templates within the view’s folder as before, as well as all nested directories within the template folder.

29 Likes

I tried both approaches (OvermindDL1 & ChrisMcCord) and they both work.
Nevertheless, using “pattern” (see Chris answer) avoids having to create new views for the subfolders, and, as such, makes our “web/views” folder much cleaner and simple.
Thank you both.

I have made the change to the view part of web.ex as suggested by mccordchris.

I have a folder that I created under lib/web/templates/layout. The name of the folder is member. In it, I have a layout file that I called member.html.eex.

Then in my controller function, I wrote this

render conn, "faqs.html", layout: {Donations.Web.LayoutView, "member/member.html"}

I am having this error

Could not render "member/member.html" for Donations.Web.LayoutView, please define a matching clause for render/2 or define a template at "lib/donations/web/templates/web/layout". No templates were compiled for this module. Assigns:

Thanks for your help. I am using phoenix 1.3

2 Likes

Those do not match? ^.^

1 Like

Also, note the glob pattern needs to be pattern: "**/*

2 Likes

Hey guys - thanks to both @chrismccord and @OvermindDL1 for the thorough overview here. I have a question about this pattern, though. Are there any drawbacks to using the root use Phoenix.View approach here in lieu of the use MyApp.Web , :view approach. Is there a way to do this with MyApp.Web or are these mutually exclusive?

Thanks in advance for any comments here.

Mike Zazaian

1 Like

Also, now that I look at it, I’m wondering if this approach is different with Phoenix 1.3-rcX. Are there any substantive changes here? I’ll probably end up digging into the Phoenix.View module here anyway, but I figured it wouldn’t hurt to ask.

Thanks again,
Mike

Hey all - so for my purposes I’ve resolved this problem. Rather than trying to work around the use MyApp.Web, :view call, I went straight to the source and added the pattern option directly to the primary MyApp.Web.view definition:

  # /lib/my_app/web/web.ex
  def view do
    quote do
      use Phoenix.View, root: "lib/my_app/web/templates",
        pattern: "**/*",
        namespace: MyApp.Web
      ..
     end
   end

This allowed me to create multiple sub-dirs within each view root directory (for my purposes this was web/templates/page), and then to call renders inside of master index.html template using relative paths from that root directory:

# /lib/my_app/web/templates/page/index.html.haml
# please note that all of my view templates use HAML  
.container
  -# Top Section
  = render MyApp.Web.PageView, 'sections/_top_section.html'

  -# More Stuff Section
  = render MyApp.Web.PageView, 'sections/_more_stuff.html'

  -# Final Stuff Section
  = render MyApp.Web.PageView, 'sections/final_stuff.html'

Additionally, because I ended up with about 12 partials overall, and because my app name was pretty long, I decided to alias the PageView module within itself for easier partial calls:

# /lib/my_app/web/views/page_view.ex
defmodule MyApp.Web.PageView do
  use MyApp.Web, :view

  alias MyApp.Web.PageView
end

This made my primary index template a big more svelte:

# /lib/my_app/web/templates/page/index.html.haml
.container
  -# Top Section
  = render PageView, 'sections/_top_section.html'

  -# More Stuff Section
  = render PageView, 'sections/_more_stuff.html'

  -# Final Stuff Section
  = render PageView, 'sections/final_stuff.html'

Anyway, happy to answer any questions, or field any fire for doing any of these things incorrectly according to convention. Also please note that I’m using Phoenix 1.3-rc2, so this may be different for Phoenix 1.2.4 apps.

6 Likes
# /lib/my_app/web/web.ex
def view do
  quote do
    use Phoenix.View, root: "lib/my_app/web/templates",
      pattern: "**/*",
      namespace: MyApp.Web
    ..
   end
 end

Doesn’t using Phoenix.View like this mean that if you define a Page.SectionView you could either use render PageView, 'sections/_top_section.html' or render Page.SectionView, '_top_section.html'?

Not trying to advocate against your approach but just trying to think through the implementation (and make sure I’m understanding everything myself!).

1 Like

If you were to use the PageView render, it would call the template path /templates/page/sections/_top_section.html.eex. If you were to use the SectionView, it would call the template path /templates/section/_top_section.html.eex.

Edit: Remember, the path is always relative to the view being used. The PageView is prefaced with /page and the SectionView is prefaced with /section. So when you call render using the PageView and set the path as /sections/file.html, remember that it’s prefaced with /page, making the actual path /page/sections/file.html.

1 Like

Hey there @axelson - thanks for the question! I’m new-ish to using Phoenix in practice myself so I’ll do my best to field this one.

@steve’s answer seems to be correct at least relative to what you may have meant. We’re only using web/templates/page as the presumed root dir for our view because we’re using the PageView to render templates.

That said, it seems like maybe you’re asking if adding namespaces to the view module itself would presume a new directory for each namespace in the template path. Is that correct? I’m not sure if that was just a typo.

If that’s what you meant, if you added a Page namespace to your SectionView, i.e. MyApp.Web.Page.SectionView, then it would automatically look for templates in web/templates/page/section, but not web/templates/page/sections. The directory structure is simply mirroring the camel-cased portion of the module before View. Consider:

# web/views/page/section_view.ex
module MyApp.Web.Page.SectionView do
  use MyApp.Web, :view
  # by default will look for templates in the root dir web/templates/page/section
end

However if you named your view MyApp.Web.Page.SectionsView, then both PageView and Page.SectionsView would use templates in the page/sections directory, though only the Page.SectionsView module would use that directory as its template root.

Does that answer your question? I’m glad to be discussing this because I’m learning a lot myself.

1 Like

This was very very useful. Thank you.

1 Like

How has this changed with 1.7+?