We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
`attr` turns component bugs into compile errors
almirsarajcic
A function component is just a function taking assigns, which means a typo’d or forgotten assign fails the way any missing map key fails: at render time, in production, on whichever page happens to call it wrong. attr and slot move that check to compile time, and they check the call site — the place that actually has the bug.
defmodule MyAppWeb.Components do
use Phoenix.Component
attr :user, :map, required: true
attr :size, :string, default: "md", values: ~w(sm md lg)
slot :actions
def user_card(assigns) do
~H"""
<div class={"card card-#{@size}"}>
<span>{@user.name}</span>
<div :for={action <- @actions}>{render_slot(action)}</div>
</div>
"""
end
end
Call it wrong and the compiler tells you before you ever load the page:
<.user_card size="xl" />
warning: missing required attribute "user" for component MyAppWeb.Components.user_card/1
warning: attribute "size" in component MyAppWeb.Components.user_card/1 must be one of ["sm", "md", "lg"], got: "xl"
Both warnings point at the caller’s line, not at the component. That is the whole value: the component is fine, the call site is wrong, and the error names the call site.
What each option buys you
required: true catches the forgotten assign. default: removes the assigns[:size] || "md" dance from the body — the default is applied before your function runs, so @size is always set. values: is the one people miss: it turns a free-form string into a closed set, so a renamed variant produces warnings at every stale call site instead of silently rendering a card-xl class that no CSS defines.
Typing the attribute as a struct tightens it further:
attr :user, MyApp.Accounts.User, required: true
attr :on_close, :any, default: nil
attr :rest, :global, include: ~w(form target)
:global is what lets callers pass phx-click, data-*, or aria-* straight through to your root element via {@rest} without you declaring each one — and include: extends the allowed set for attributes Phoenix does not treat as global by default.
Slots get the same treatment
slot :inner_block, required: true
slot :column, doc: "a table column" do
attr :label, :string, required: true
attr :sortable, :boolean
end
Attributes declared inside a slot block are validated on each slot entry, so <:column> without a label reports the same way:
warning: missing required attribute "label" in slot "column" for component MyAppWeb.Components.table/1
There is a second payoff beyond correctness. Because these declarations are introspectable, mix docs renders a full attribute table for every component automatically — the documentation stops being a thing you maintain by hand and starts being a thing you cannot forget to update.
The migration is incremental. Adding attr to one component changes nothing for the others, and each one you annotate immediately starts reporting its own bad call sites.
copied to clipboard