defmodule Headless do
use Phoenix.Component, global_prefixes: ~w(x-)
@components [:avatar, :clipboard, :popover, :toggle]
@doc false
def components, do: Enum.into(@components, %{}, &{&1, apply(__MODULE__, &1, [])})
@doc """
Avatar component
## Elements
- `root` - root element
- `image` - `` element
- `fallback` - fallback element
## Example
defmodule ExampleAvatarComponents do
use Phoenix.Component
import Headless
attr :src, :any
attr :alt, :any
attr :initials, :string
def avatar(assigns) do
~H\"\"\"
<.use_avatar :let={a} src={@src}>
@form[:email]"
attr :id, :string, default: nil
attr :name, :string
attr :value, :string
attr :type, :string, default: "text"
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
attr :rest, :global, doc: "additional attributes to be passed to the input element"
slot :inner_block
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
assigns
|> assign(field: nil, id: assigns.id || field.id)
|> assign(:errors, field.errors)
|> assign_new(:name, fn -> field.name end)
|> assign_new(:value, fn -> field.value end)
|> input()
end
def input(%{type: "checkbox"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
end)
~H"""
"""
end
def input(assigns) do
~H"""
"""
end
@doc """
Render input errors
Heavily based on default Phoenix generated CoreComponents
Storybook: No errors
<.input_error field={@form[:name]} :let={{msg, _opts}}>
<%= msg %>
Storybook: With errors
<.input_error field={@form[:email]} :let={{msg, _opts}}>
<%= msg %>
"""
attr :field, Phoenix.HTML.FormField,
required: true,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
slot :inner_block,
required: true,
doc: "The default slot will be rendered with {message, options} tuple"
def input_error(assigns) do
~H"""
<%= for error <- @field.errors do %>
<%= render_slot(@inner_block, error) %>
<% end %>
"""
end
defp make(assigns, component) do
render(assigns, component)
end
defp render(assigns, nodes) do
nodes =
Enum.into(nodes, %{}, fn {node, attrs} ->
{node,
Enum.into(attrs, %{}, fn
{k, s} when is_binary(s) -> {k, s}
{k, a} when is_atom(a) -> {k, assigns[a]}
end)}
end)
assigns = assign(assigns, nodes: nodes)
~H"""
<%= render_slot(@inner_block, @nodes) %>
"""
end
@doc """
Replace `def` with `defc` to optimize component rendering by inlining static attributes.
## Compile-time validation
Using `defc` will also check in compile-time if the referenced attributes are correct.
This definition (using `def`) will compile and crash in runtime due to undefined `wrong` node reference.
def toggle(assigns) do
~H\"\"\"
<.use_toggle :let={t}>
<.input {t.wrong}/>
\"\"\"
end
# => ** (KeyError) key :wrong not found in: (...)
When using `defc` it will not compile:
defc toggle(assigns) do
~H\"\"\"
<.use_toggle :let={t}>
<.input {t.wrong}/>
\"\"\"
end
# => ** (Phoenix.LiveView.Tokenizer.ParseError): Unknwon node "wrong" for component use_toggle
## Example
defmodule ExampleAvatarComponents do
use Phoenix.Component
import Headless
attr :src, :any
attr :alt, :any
attr :initials, :string
defc avatar(assigns) do
~H\"\"\"
<.use_avatar :let={a} src={@src}>