I am building a Button component with LiveView 0.18.3 that should render either a <button> or an <a> tag depending on whether a url attribute is present:
defmodule AppWeb.Sirius do
use Phoenix.Component
attr :url, :string, default: nil
def button(assigns) do
assigns =
if assigns[:url] != nil do
assign(assigns, :tag, "a")
else
assign(assigns, :tag, "button")
end
~H"""
<.dynamic_tag name={@tag} href={@url} class="Sirius-Button">
<%= render_slot(@inner_block) %>
</.dynamic_tag>
"""
end
end
The compiler warns me about href being an undefined attribute:
Looking at the LiveView code, href is indeed not in the globals list triggering the warning (phoenix_live_view/lib/phoenix_component/declarative.ex:1090).
Aside from the warning, the component does work as expected, either rendering out a <button class="Sirius-Button">...</button> when no url is present, or an <a href="..." class="Sirius-Button>...</a> when url is present.
Any ideas what can I could do to the get rid of the compiler warning?
Update: made some progress today re-defining a “custom” version of the dynamic_tag component included with LiveView 0.18.3 and injecting href into globals. Would be much nicer if we could pass additional includes for :rest to the dynamic_tag component that ships with LiveView
defmodule AppWeb.Components.Sirius.DynamicTag do
use Phoenix.Component
@doc type: :component
attr :name, :string, required: true, doc: "The name of the tag, such as `div`."
attr :rest, :global,
include: ~w(href),
doc: """
Additional HTML attributes to add to the tag, ensuring proper escaping.
"""
def dynamic_tag(%{name: name, rest: rest} = assigns) do
tag_name = to_string(name)
tag =
case Phoenix.HTML.html_escape(tag_name) do
{:safe, ^tag_name} ->
tag_name
{:safe, _escaped} ->
raise ArgumentError,
"expected dynamic_tag name to be safe HTML, got: #{inspect(tag_name)}"
end
assigns =
assigns
|> assign(:tag, tag)
|> assign(:escaped_attrs, Phoenix.HTML.attributes_escape(rest))
if assigns.inner_block != [] do
~H"""
<%= {:safe, [?<, @tag]} %><%= @escaped_attrs %><%= {:safe, [?>]} %><%= render_slot(@inner_block) %><%= {:safe, [?<, ?/, @tag, ?>]} %>
"""
else
~H"""
<%= {:safe, [?<, @tag]} %><%= @escaped_attrs %><%= {:safe, [?/, ?>]} %>
"""
end
end
end
@kartheek yes that can work for simple components, but the ~H markup (outside from the simplified example I posted) is a lot more involved, and duplicating the entire embedded heex template with the only difference being the tag_name make it a lot harder to maintain. Because then I need to remember to change markup in multiple embedded templates …
If you don’t want to duplicate inner_block of the component, you can wrap it with another component like LostKobrakai’s example.
Code in second post of yours is a heex wrapper over eex template. I would prefer to stay in heex and leverage components like link/1 than dynamically generating parts of html tag. Initially when migrating to heex, I was trying some things with eex and generating tags - but now everything is mostly heex.