Is it possible to dynamically "render" HTML tags with Phoenix Component?

In my project we have a Text component that respects the design system implementation with different sizes and line-heights. Then our typography has these variations:

  • h1…h5
  • base
  • large
  • medium
  • small

for base, medium and small variations we can use <span /> or <p/> tag. However, for h1..h5 variations it would be better to respect HTML semantics using those exact tags.

So I thought: I can pattern match on the prop size of the text component and then render something based on it. Or use flow control in heex tempaltes, that way:

attr :size, :string, values: ~w(h1 h2 h3 h4 h5 base lg md sm), required: true

def text(assigns) do
  ~H"""
  <h1 :if={@size == "h1"}> ... </h1>
  <h2 :if={@size == "h2"}> ... </h2>
  <h3 :if={@size == "h3"}> ... </h3>
  <!-- go for each variation -->
  """
end

But this code is kinda long… So I tried to render dynamically:

defp text_component(variation) do
  case variation do
    "h1" -> ...
  end
end

def text(assigns) do
  ~H"""
  <!-- what goes here? I want to call text_component(@size) and get a HTML tag rendered -->
   """
end

I know I can render function components dynamically with apply/3 or even with Phoenix.LiveView.HTMLEngine.component/3, but how about native HTML tags? I would need to define a micro component to render the tag? maybe:

def h1(assigns) do
  ~H"<h1 {...@assigns}> ... </h1>"
end

You can do something like this:

  attr :size, :string, values: ~w(h1 h2 h3 h4 h5 base lg md sm), required: true

  def text(%{size: "h" <> _} = assigns) do
    ~H"""
    <%= Phoenix.HTML.Tag.content_tag @size do %>
      ...
    <% end %>
    """
  end

  def text(assigns) do
    ~H"""
    <span>...</span>
    """
  end

or

  def text(%{size: "h1"} = assigns) do
    ~H"""
    <h1>...</h1>
    """
  end

  def text(%{size: "h2"} = assigns) do
    ~H"""
    <h2>...</h2>
    """
  end

if you want to avoid using content_tag

This seems like an odd approach to me - why is <.text size="h5">words words words</.text> preferable to <h5>words words words</h5>, especially if semantics are important?

A .text component that could be either a block element or an inline element (if some size expands to span) also seems strange to me.

Phoenix.Component provides <.dynamic_tag name={…}></.dynamic_tag>

10 Likes

Wow! That’s exactly what I was searching! Thanks @chrismccord!