Phoenix for Django Devs: The {% block %} Tag
by Justin Michalicek on Sept. 5, 2016, 4:05 p.m. UTCLast night I came up with a solution for the specific problem of page or template specific javascript in Phoenix, which I posted here. When I woke up this morning I realized that the more generic problem was simulating the {% block %} template tag. So to expand upon the example from my previous post, a top level parent Django template (without lots of elements to keep it clear) might look like this.
base.html:
<html> <body> Common page elements and text here {% block content %}Page specific content gets displayed here.{% endblock content %} Some more common page elements. <!-- Now another block with default text which we may want to override per page --> {% block some_stuff %}More standard stuff for every page with default content which might get overridden by a page's template goes here{% endblock some_stuff %} <!-- now the page specific javascript --> {% block page_js %}{% endblock page_js %} </body> </html>
And then child templates for pages would be something like this to override the content and page_js blocks but use the default from the some_stuff block.
one_block_not_overridden.html:
{% extends 'base.html' %} {% block content %} Stuff here {% endblock content %} {% block page_js %} <script src="https://example.org/some_external_script.js"></script> <script> alert("Page specific annoying alert!") </script> {% endblock page_js %}
Or perhaps to override all three blocks a template might look like this.
all_blocks_overridden.html:
{% extends 'base.html' %} {% block content %} Stuff here {% endblock content %} {% block some_stuff %}I don't like the default some_stuff block for this page. It needs custom content.{% endblock some_stuff %} {% block page_js %} <script src="https://example.org/some_external_script.js"></script> <script> alert("Page specific annoying alert!") </script> {% endblock page_js %}
Because of Phoenix' use of composition from the top level down, inserting lower level templates, rather than Django's OO based templating where children inherit from and then override the parent a way to accomplish this was not immediately obvious. The standard base page template for phoenix looks like this, and when it is rendered you cannot pass data up to it/override it in the lower level templates.
base.html.eex:
<html> <body> Common page elements and text here <%= render @view_module, @view_template, assigns %> Some more common page elements such as a site footer, etc. here. <!-- Load jquery, etc. whatever common js to every page here --> <!-- but our specific view is being dealt with up in the render above, so it has no way of injecting more content further down the template, such as here --> </body> </html>
Based on the page specific javascript solution I made a more generic function to handle this, called page_block/2 below as well as a default_page_block/2 to provide a default.
defmodule MyApp.LayoutView do use MyApp.Web, :view # Add this function here @doc """ Render a template as a block anywhere in the page. """ def page_block(block_name, conn) do view_template = conn.private[:phoenix_template] view_module = conn.private.phoenix_view block_template = "#{view_template}_#{block_name}.html" try do raw render_to_string(view_module, block_template, assigns: conn.assigns) rescue Phoenix.Template.UndefinedError -> default_page_block(block_name, conn) end end @doc """ Provide a default template and value for a block which may be overridden by the view. """ defp default_page_block(block_name, conn) do block_template = "#{block_name}.html" try do raw render_to_string(__MODULE__, block_template, assigns: conn.assigns) rescue Phoenix.Template.UndefinedError -> "" end end end
Then in my templates/app.html.eex I just these lines wherever I want them to appear and be able to be overridden.
templates/app.html.eex:
<%= page_block "page_js", @conn %> <%= page_block "some_stuff", @conn %>
Giving us the following
base.html.eex:
<html> <body> Common page elements and text here <%= render @view_module, @view_template, assigns %> Some more common page elements here. <!-- now some stuff I may want to override --> <%= page_block "some_stuff", @conn %> <!-- Load jquery, etc. whatever common js to every page here --> <!-- page specific javascript stuff will now load here --> <%= page_block "page_js", @conn %> </body> </html>
Now for any template I can just add one with the same name with _scripts.html on it. So for an edit view with a template named myapp/templates/a_model_name/edit.html.eex, I just add myapp/templates/edit.html_scripts.html.eex. A default template can be added to myapp/templates/layout/page_js.html.eex and myapp/templates/layout/some_stuff.html.eex.