With an eye to the future that is being laid out by Phoenix LiveView 0.16 and the introduction of HEEx
templates, we’ve recently begun transitioning to using function components, rendered with the component/3
macro, instead of stateless LiveComponents.
Marlus Saraiva lays out a pretty good roadmap on the Dashbit blog, but we’re finding that when implementing these stateless components, we still have a lot of unanswered questions, which Marlus raises in his post: When defining a function component, how should we declare:
- which assigns are public and can (or must) be passed to
component/3
? - what’s the type of each assign?
- which assigns are required? Will they receive default values if I don’t initialize them?
- which assigns represent the internal state of the component and shouldn’t be touched at all?
Without answers to these questions and armed only with the guidance that a function component must accept a map of assigns and return a ~L
(or ~H
) template, we’ve been … making it up as we go along with code like this:
defmodule MyApp.Components.Checkbox do
use Phoenix.HTML
import Phoenix.LiveView.Helpers
@default_assigns %{
__changed__: nil,
class: nil,
form: :checkbox,
id: nil,
label: nil
}
@default_attrs []
def checkbox(assigns) do
{assigns, attrs} = Map.split(assigns, Map.keys(@default_assigns))
attrs = Keyword.merge(@default_attrs, Map.to_list(attrs))
assigns
|> Enum.into(@default_assigns)
|> render(attrs)
end
defp render(assigns, attrs) do
~L"""
<div class="flex flex-row items-center checkbox <%= @class %>">
<%= checkbox @form, @id, attrs %>
<%= label @form, @id, @label, class: "whitespace-nowrap" %>
</div>
"""
end
end
What the code is attempting to do is primarily account for optional assigns, and set their defaults if they’re not supplied by the caller. But in addition, it’s defining all of the assigns that the component consumes as arguments and splitting everything else out as attributes to be passed to the underlying HTML. So in the above example:
<%= component &Checkbox.checkbox/1,
id: :click_me,
label: "Click me",
phx_click: "click",
phx_target: "#something" %>
would be rendered as
<div class="flex flex-row items-center checkbox">
<input id="checkbox_click_me" name="checkbox[click_me]" type="checkbox" phx_click="click" phx-target="#something">
<label class="whitespace-nowrap" for="checkbox_click_me">Click me</label>
</div>
Repeating these assigns/attribute gymnastics in every component is obviously a bad idea, but right now there’s little guidance as to what the best practice is when defining these function components.
What’s the right way? Discuss