Phoenix for Django Devs: Per Page JavaScript Without The Mess
by Justin Michalicek on Sept. 5, 2016, 3:47 a.m. UTCWhen I work on a Django project I normally have a simple block at the bottom of my base template which is empty, something like so:
base.html:
<html> <body> Common page elements and text here {% block content %}{% endblock content %} Some more common page elements such as a site footer, etc. here. <!-- Load jquery, etc. whatever common js to every page here --> <!-- now this block is overridden in child templates to load page specific external js or in page js --> {% block page_js %}{% endblock page_js %} </body> </html>
And then child templates for pages would be something like
child.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 %}
It was not super clear how to do that with Phoenix, though, because it does not use inheritance. Instead it uses composition, injecting your "child" templates into the main app.html.eex template. So I hit up google and came across over complicated solutions involving using webpack or making changes to allow brunch to load page specific modules, requiring changes to several files and had limitations such as only including a single .js file for a page rather than allowing multiple <script> tags for loading page specific external js files or very small in page js.
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.
<html> <body> Common page elements and text here <%= render @view_module, @view_template, assigns %></code> 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>
So I came up with my own solution. In web/views/layout_view.ex I add a simple function, page_scripts/3:
defmodule MyApp.LayoutView do use MyApp.Web, :view # Add this function here @doc """ Takes a view module, template currently being rendered, and the assigns. Uses the view module to load a page specific template for javascript where the view template name has _scripts.html appended to it. """ def page_scripts(view_module, view_template, assigns) do scripts_template = "#{view_template}_scripts.html" try do raw render_to_string(view_module, scripts_template, assigns: assigns) rescue Phoenix.Template.UndefinedError -> "" end end end
Then in my templates/app.html.eex I just add this one line:
<%= page_scripts @view_module, @view_template, assigns %>
Giving us the following
<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 --> <!-- page specific javascript stuff will now load here --> <%= page_scripts @view_module, @view_template, assigns %> </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/edit.html.eex, I just add myapp/templates/edit.html_scripts.html.eex. I imagine that could have the .html after _scripts removed for edit.html_scripts.eex or even have the function look for edit_scripts.html.eex, etc.