How can one render a Liveview .heex file dynamically?

I have a small side project I am working on for someone else and one feature they are looking for is the ability to create liveview pages dynamically on a staging/production running app via an admin panel (the user will enter a name and the slug and module name will be created dynamically based on that)

What I’m trying to figure out is if I were to create the most basic liveview file (mount and render function), is there a way to also compile its corresponding beam file in runtime?

I saw that there is a way to pass in a .beam file to the load path, so calling it seems feasible, it’s just the actual generating part I am confused about.

2 Likes

I would strongly suggest using some other mechanism for dynamic templates (like mustache) other than heex. Dynamic code loading, even done by admins, is a great way to cause all kinds of trouble because, at the end of the day, you’re enabling arbitrary code execution within your application.

FWIW just that you’d be using something like mustache doesn’t mean it can’t be live as long as it’s rendered inside of a live view that knows how to trigger a re-render of the mustache content.

6 Likes

+1 on using a safe templating language, you could evaluate the solid library which uses liquid, the same language as jekkyl

5 Likes

Thanks @benwilson512! Yea, I had a feeling that was the case, but didn’t want to limit the context of question with that presumption.

In relation to the content still being live, and pardon if I interpret this wrong, are you saying I could use a custom liveview module that renders whatever mustache content based on some parameter (the current route, for example)? Essentially, if I give the user the ability to create a page, under the hood I could just store that slug value and pass that to the existing liveview module and render the content for that route?

(Big fan of the graphql work btw, that piece was the first tooling I used when I got started and got me hooked on elixir/phoenix!)

I heard of liquid, but wasn’t aware of solid! This looks like the perfect tool for my use case, thanks a bunch @mayel!

1 Like

I have a similar problem I’ve been trying to come up with a solution to.

We have a product where a good chunk of the application is built from configuration (YAML). A lot of the configuration is backed by the wonderful solid library.

The configuration is a mixture of HTML + liquid templating. This has worked really well up to the point of wanting to integrate with heex. We want to have the ability to continue to define these small chunks of templating using the liquid template syntax, but also have the ability to incorporate components from Phoenix and LiveView into these small chunks of templating.

I have gotten as far as compiling the liquid template strings to heex using EEx.compile_string/2 and the Phoenix.LiveView.HTMLEngine, but where things begin to go awry is when validation kicks in and begins to conflict with some of the liquid syntax. For example:

<div>
  {% if age < 21 %}
    <.example_component>You are not 21</.example_component>
  {% endif %}
</div>

The validator raises an error due to the < in the liquid conditional, causing it to believe there is a missing tag name.

I can overcome this by simply doing a double render – render the liquid template first, then pass the rendered string to the EEx compiler; the issue with this is if there’s any invalid HTML in the liquid template, it’s going raise an error on page visit.

Ideally, it’d be nice if there was a way to tell the HTML engine for the heex compiler that you want to bypass validating the HTML.

1 Like

I don’t understand what you’re proposing as an alternative - your “double render” is what I’d expect to see when combining these two things:

  • process Liquid tags at configuration-time
  • the result is valid Heex, which is compiled

The Heex compiler wouldn’t ever see the Liquid markup in this setup.

1 Like

Thanks for the reply al2o3cr!

My biggest issue really boils down to not being able to take advantage of Heex’s validation during compile time. Perhaps if I give an example, that might provide some clarity:

template_string = ~s"""
<div>Hello <strong>{{ name }}, how are you today?</div>
"""

# This is done at compile time
{:ok, parsed_template} = Solid.parse(template_string)

# iex>
# %Solid.Template{
#   parsed_template: [
#     text: ["<div>Hello <strong>"],
#     object: [argument: [field: ["name"]], filters: []],
#     text: [", how are you today?</div>\n"]
#   ]
# }

# Following is done at runtime
rendered_template =
  parsed_template
  |> Solid.render(%{"name" => "John"})
  |> to_string()
  
# iex>
# "<div>Hello <strong>John, how are you today?</div>\n"

EEx.eval_string(rendered_template, [assigns: %{}], engine: Phoenix.LiveView.HTMLEngine) |> Phoenix.HTML.Safe.to_iodata()

# iex>
# ** (Phoenix.LiveView.HTMLTokenizer.ParseError) nofile:1:44: unmatched closing tag. Expected </strong> for <strong> at line 1, got: </div>

I don’t see a way of compiling the template into Heex at compile time as it’s dependent on the template being rendered by solid which can’t be done until run time.

If I have a template with something like the following, that I attempt to compile at compile time, it would fail due to the liquid syntax.

template_string = ~s"""
<div>
  {% if age < 21 %}
    You are not old enough
  {% else %}
    You are old enough
  {% endif %}
</div>
"""

# Do a check during compile time to validate HTML
EEx.compile_string(template_string, engine: Phoenix.LiveView.HTMLEngine)

# ** (Phoenix.LiveView.HTMLTokenizer.ParseError) nofile:2:14: expected tag name

So ultimately I am left with having to run non-validated HTML if I want to use Heex, which can lead to surprise 500s.

The only way to enable validation would be to separate your templates entirely. If you have time to play you can create a custom engine. To achieve this a custom template on top of all templates could be used, example:

<div>
  <solid>
  {% if age < 21 %}
    You are not old enough
  {% else %}
    You are old enough
  {% endif %}
  </solid>
</div>

Then you can run your custom engine, that would generate an output like:

<div>
  <% {:ok, parsed_template} = Solid.parse("
 {% if age < 21 %}
    You are not old enough
  {% else %}
    You are old enough
  {% endif %}
") %>
  <%= Solid.render(parsed_template, solid_assigns) %>
</div>

The only limitation of this solution is that you can’t partially inject html from your solid templating, but this shouldn’t be the case in the first place.

1 Like

As you’ve seen the way solid parses templates is not directly compatible with EEx or heex. You’d need to convert the parsed solid template to valid heex before passing to to the heex compiler. That should be possible, but I don’t think that’s built in. Basically many of the things happening within Solid.render/2 would need to be applied through heex syntax / individual function calls instead.

1 Like

Unless I’m mistaken you say that these solid templates live inside configuration files. Are these configuration files known at build/compile time?

If yes, then you might be able to use the 2-pass compilation idea by introducing an extra mix compiler inside mix.exs

That compiler would first compile and evaluate the solid snippets, then inject them into your heex views. Then the regular EEx compiler would take over and still be able to detect any malformed html at compile time.

Could something like that work for you?

We already do something similar to that. Currently, the solid templates gets parsed and held inside ecto schemas (these schemas don’t map to a DB, just purely used for holding parsed configuration in memory/cache), and this occurs at compile time.

That compiler would first compile and evaluate the solid snippets, then inject them into your heex views. Then the regular EEx compiler would take over and still be able to detect any malformed html at compile time.

This is where I struggle to understand. Since the solid templates get parsed into a syntax specific to Solid and its renderer; any HTML therein would not have its full structure known until it’s fully rendered which is dependent on assigns available only at run time.

I don’t believe that’s necessarily a stopper, since the same issue exists with Heex, hence why you can’t do something like this (very) contrived example:

<%= if true do %>
  <div class="some-class-true">
<% else %>
  <div class="some-class-false">
<% end %>
  ....
</div>

I can already achieve this by simply doing something like:

template_string = Solid.parse("<h1>Hello {{ name }}</h1> <.my_component />")

# Parsed string above (done at compile time, held in configuration)
result =
  template_string
  |> Solid.render(%{"name" => "John"})
  |> to_string()
  |> EEx.eval_string(
    [assigns: %{}],
    functions: [{Components.Helpers, Components.Helpers.__info__(:functions)}],
    engine: Phoenix.LiveView.HTMLEngine
  )

# Heex template
assigns = %{result_example: result}
~H|<%= @result_example %>|

This works. The issue then becomes if there’s any invalid HTML in that solid template, it’s going to raise an error when it’s eval’d.

I’m just trying to think through any cases where that could potentially be avoided / surfaced at compile time. And I fear the answer is no, at least not without getting into the potential of writing our own parser.

1 Like

Ok, just to confirm: any interpolation of values inside the solid templates is done at compile time, so at compile time you have the plain html snippet produced by the Solid library, and it’s that plain html snippet that needs to be included in heex templates. Also that plain html snippet might be malformed.

Assuming that I got it right, why not pass it through the heex engine while still at compile time?

The custom compiler I alluded to before would do just that: grab the snippet, put it in a heex template and compile/render it. If malformed, the compiler would complain while still compiling the code.

Solid does not compile to usable html in the first place. It compiles to a custom datastructure, which is only assembled back to html whenever you provide the runtime data to embed.

Thanks for the suggestion, this is one worth exploring, I believe.