I’m trying to dynamically render some function components for my navigation menu and am looking for a way to potentially do it a little… nicer?
What I have right now is:
# in module OutlineIcons
def home(assigns) do
assigns = assign_defaults(assigns)
~H"""
<svg id={@id} class={@class} style={@style} xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
"""
end
# in module Page
def page(assigns) do
navigation = [
{"Home", Routes.home_index_path(assigns.socket, :index), &OutlineIcons.home/1}
]
# add it to assigns, lots of markup...
~H"""
<%= for {label, href, icon} <- @navigation do %>
<%= live_redirect to: href, class: "text-gray-600 hover:bg-gray-50 group flex items-center px-3 py-2 text-sm font-medium rounded-md" do %>
<%= icon.(%{__changed__: %{}, class: "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"}) %>
<%= label %>
<% end %>
<% end %>
"""
end
Now, since my icon component has an assigns map as parameter, I need to create my own one by adding %{__changed__: %{}. This works and renders just fine, but seems verbose. Is there a nicer way to do this?
After having read more about function capturing (Modules and functions - The Elixir programming language), I changed my approach. Just like OP, I now pass captured functions to my navigator component (&home_icon/1), instead of function name strings (“home_icon”).
I was sort of hoping maybe something like <.{icon} color="red"/> would work to call a function component dynamically, but the component macro gets the job done.
What’s the best way to generate the assigns, since a simple map of attrs won’t work?
And, is this still considered a good practice? Debating whether just wrapping the component I want to call dynamically (Phoenix.Component.link) in another component would be better…
It doesn’t seem to accept a Keyword list either. Passing a list results in this error: ** (BadMapError) expected a map, got: [navigate: "/test"] while changing to a map gives me this: assign/3 expects a socket from Phoenix.LiveView/Phoenix.LiveComponent or an assigns map from Phoenix.Component as first argument, got: %{__given__: %{navigate: "/test"}
The second error also explicitly mentions calling a function component dynamically, but seems to assume the only valid use case would be in tests:
If you are using HEEx, make sure you are calling a component using:
<.component attribute={value} />
If you are outside of HEEx and you want to test a component, use Phoenix.LiveViewTest.render_component/2:
Phoenix.LiveViewTest.render_component(&component/1, attribute: "value")
Oh, maybe try something like apply(Phoenix.Component, link, [%{patch: ~p"/details"}] since apply/3 takes a list of a function’s arguments as its third parameter and Phoenix.Component.link/1 has an arity 1 with the assigns map as its only parameter.
@tfwright what are you trying to do? You’re generally not expected to create assigns out of thin air, because those wouldn’t be able to be change tracked.
I am conditionally rendering a link in a function kind of like this
defmodule MyComponent do
def render(assigns) do
~H"""
<%= Helpers.do_something(@foo, @bar) %>
"""
end
end
defmodule MyHelpers do
def do_something(foo, bar) do
if foo do
render_link
else
"some text"
end
end
end
In the past I would have used an HTML helper function to accomplish this.