Conditionally rendering a non-blank HEEX template slot

You probably already know that <span>{nil}</span> in a HEEX template produces <span> </span> when rendered. I find this very inconvenient because I can’t conditionally display a placeholder content via the :empty CSS pseudo-class, as it doesn’t tolerate whitespace at all. I know, there’s :blank but it’s still experimental and it’s not supported by any browser.

I encountered an article which tries to demonstrate how to check if a slot is empty. The solution falls apart as soon as you call slot_empty? with a slot from a list of slots. The following fragment makes the function report that both slots are not empty while in fact they are.

<.some_component>
  <:item></:item>
  <:item></:item>
</.some_component>

The following macro is inspired by Phoenix.Component.render_slot/2 and actually renders the slot and checks if it’s all whitespace. If it is, then it returns nil, otherwise - the rendered slot, ready to be put in the template.

defmacro maybe_render_slot(slot, argument \\ nil) do
  # This is what `Phoenix.Component.render_slot/2` does with the twist
  # that our function can be used outside of a HEEX template.
  changed =
    if Macro.Env.has_var?(__CALLER__, {:changed, Phoenix.LiveView.Engine}) do
      Macro.var(:changed, Phoenix.LiveView.Engine)
    end

  quote do
    rendered =
      unquote(changed)
      |> Phoenix.Component.__render_slot__(unquote(slot), unquote(argument))
      |> Phoenix.HTML.Safe.to_iodata()

    blank? =
      rendered
      # Using Erlang's API because it accepts IO lists and we can avoid
      # "materialising" the IO list to a binary. Of course, one can compile
      # the regular expression but I skipped it for brevity sake.
      |> :re.run("^[[:space:]]*$")
      |> then(&match?({:match, _}, &1))

    if not blank? do
      Phoenix.HTML.raw(rendered)
    end
  end
end

Then, in a template, you can use the macro to conditionally render a non-blank slot and use a sibling element as a placeholder content.

<p :for={item <- @items}>
  <span :if={rendered = maybe_render_slot(item)}>{rendered}</span>
  <span class="hidden only:inline">{@placeholder}</span>
</p>

I hope someone finds this helpful. I can’t wait for the browsers to support the :blank CSS pseudo-class so we don’t need to do such template gymnastics.

2 Likes