Loading heex templates in LiveView based on an assigns variable

Let’s say I have a Choose Your Own Adventure Book as static HTML files, numbered 001.html through 100.html, all with HTML links connecting them together in a directed graph.

I’d like to build a LiveView project with the same functionality. Assume the HTML files are long enough that I want to keep the contents in their own files, not in an in-line template inside the LiveView.

I thought I could use a LiveComponent with the name of the template file to be rendered somewhere in the assigns, and then render it with something like Phoenix.View.render/3 – but the Phoenix.View module isn’t available to LiveView and I don’t want to go messing around adding it in myapp_web.ex without knowing what I’m doing.

I also tried dynamically loading a component with <MyComponentsModule.#{@section_number} />, but that’s just an invalid tag.

I do feel like I’m missing something obvious here. What’s the idiomatic way to do this?

Solution found, although I think it’s ugly. (Seriously, if there’s a prettier way to do this I’d love to see it.)

I render a generic ‘wrapper’ component in the LiveView, with the number of the entry I want to enter in the assigns.

  def render(assigns) do
    ~H"""
    <MyComponent.read entry={@entry} />
    """
  end

Clicking a link in an entry sends a new entry number into the LiveView. I sanitize it by running it through String.to_integer/1, and then get a function name with (shudder) Code.eval_string.

  def handle_event("click", %{"entry" => entry}, socket) do
    entry = String.to_integer(entry)
    {entry, _} = Code.eval_string("&MyAppWeb.MyComponent.entry_#{page}/1")
    
    {:noreply, assign(socket, entry: entry)}
  end

Over in the component, I use Phoenix.LiveView.HTMLEngine.component/3 to call the sent-in function.

  def read(assigns) do
    ~H"""
    <%= Phoenix.LiveView.HTMLEngine.component(
      @entry, [], {__ENV__.module, __ENV__.function, __ENV__.file, __ENV__.line}
    ) %>
    """
  end

I also call embed_templates "entries/*", which allows that component/3 call to find the individual .heex files. And then it works.

This was a pain to figure out, looks ugly, and even though I think that String.to_integer/1 will sanitize anything malicious sent into the Code.eval_string function, it still freaks me out a bit to have it there. Any better ideas?

Look at live_component for dynamic rendering of live components.

https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#live_component/1

Thanks for replying. So say I have something like

<.live_component module={MyComponent} id={@entry} entry={@entry} />

in the render function of my LiveView. That I understand.

In MyComponent, how would I then conditionally load an .heex file, depending on the value of @entry? That’s the part I really struggled with, since Phoenix.View.render/3 seems to have gone away in the switch to Phoenix.Component.

I was suggesting you have a component for each so you don’t need to conditionally render a file because you are already conditionally rendering a component but you may be able to do it w/o adding in a component.

There is no problem adding Phoenix.View in. Check out the older Phoenix generators to see how that file was before Phoenix.View stopped being included.