Before I answer your question: I just want to ask if using a LiveView and a functional component is the most appropriate tool for this? You would be better served with these files being rendered to disk as plain HTML (or some other templating language, like Mustache), and serving those files directly (or applying your templating function of choice). See this post for some more details: How can one render a Liveview .heex file dynamically? - #2 by benwilson512
However, if you truly understand what you are doing (or just want to learn ), you can do what you want using Kernel.apply/3
. Again, I would not suggest this; you’re effectively allowing a requester to select a function for them to execute. Further, HEEx templates are arbitrarily powerful, and a setup like this could be used for arbitrary code execution on your server, given the right circumstances.
To set this up in the safest way possible (again, I would not consider this really that safe), I split out the components into a separate PageHTML
module. Here’s the associated directory structure.
.
├── page.ex
├── page_html
│ ├── page-1.heex
│ └── page-2.heex
└── page_html.ex
PageHTML
is quite simple in that it just uses embed_templates
, as you suggest
defmodule PhxtestWeb.PageHTML do
use PhxtestWeb, :html
embed_templates "page_html/*"
end
I defined a route for the live view as
live "/:page", PageLiveView
…which renders the following live view
defmodule PhxtestWeb.PageLiveView do
alias PhxtestWeb.PageHTML
use PhxtestWeb, :live_view
def render(assigns) do
with {:ok, template_atom} <- atom_for_page(assigns.page),
{:ok, rendered} <- try_component_render(template_atom, assigns) do
rendered
else
{:error, :notfound} -> ~H"""
<span>Not found.</span>
"""
end
end
def mount(%{"page" => page}, _session, socket) do
{:ok, assign(socket, :page, page)}
end
defp atom_for_page(page) do
try do
# By using String.to_existing_atom, we ensure that we are not leaking atoms
# with arbitrary input from a user
{:ok, String.to_existing_atom(page)}
rescue
# String.to_existing_atom throws an ArgumentError if the atom is not defined
# The atom will be defined at function-definition time for the template
# (`embed_templates` effectively just generates a function definition for you, AFAIK)
ArgumentError -> {:error, :notfound}
end
end
defp try_component_render(component_name, assigns) do
try do
rendered = apply(PageHTML, component_name, [assigns])
{:ok, rendered}
rescue
# apply throws an ArgumentError if the function is not found...
# this would technically also happen if the component threw an
# ArgumentError, but this isn't production quality code to begin with :)
ArgumentError -> {:error, :notfound}
end
end
end
Now, when someone navigates to /page-1
, the page-1
template will be rendered
and when visiting a template that doesn’t exist, they will get “Not found”
Hope this helps! Now, don’t do this