F6.05 · part 2 of 3
Function components & slots
A function component is the smallest reusable unit of markup, and it is exactly what its name says:
a function from assigns to a rendered template. You define
def course_card(assigns), return a ~H block, and call it in
any template as <.course_card course={course} /> — the leading dot marks a
local component. What makes it more than a partial is its typed interface:
attr :course, :map, required: true declares an input, and the compiler warns when a
caller forgets it or passes an unknown attribute. A slot declares a hole the caller fills with
content — render_slot(@inner_block) drops that content into place — so a
component can wrap as well as render. The payoff is the same as extracting a function anywhere: the
<li> you wrote once in F6.05.1 becomes a single
<.course_card> used in the index, the search results, and the dashboard, and a
change to the card is a change in one place.
Four parts: the three building blocks of a component, the interface it presents, a real card component in code, and composing it across templates with a slot.
Three building blocks
A component is built from three declarations. attr declares a typed input the
compiler checks. slot declares a region the caller fills with markup.
render_slot/1 renders that filled-in content where you place it. Select one.
Component building blocks · select one
block: attr
is: a typed, checked input
Declaring attr is the difference between a component and a bare partial.
The partial fails at runtime with a confusing error; the component fails at compile time with a clear one,
naming the attribute the caller forgot.
The component interface
A component call has two channels in and one out. Attributes pass typed values
(course={course}); the slot passes markup (the content between the
tags); and the component returns rendered markup. The compiler validates the attributes
against the attr declarations before the page ever renders.
attrs + slot → component → markup
Because the interface is explicit, a component is documentation. Reading its
attr and slot lines tells you everything a caller must supply,
the same way a function head tells you its arguments.
A real component
The course_card component declares a required :course map and
an optional :class string, then renders an <article>.
The list-class syntax {["card", @class]} conditionally joins classes, and
:if shows the badge only when published — the same constructs from F6.05.1, now
packaged behind a name.
# in PortalWeb.CatalogComponents — `use Phoenix.Component` attr :course, :map, required: true attr :class, :string, default: "" def course_card(assigns) do ~H""" <article class={["card", @class]}> <h3>{@course.title}</h3> <p :if={@course.published} class="badge">Published</p> </article> """ end
The component owns its markup and its inputs, nothing else. It does not fetch the course or know
where it came from — it renders whatever %Course{} a caller hands it, which is why
the same card works on every page that has one.
Composing and slots
With the card defined, the F6.05.1 list collapses to one self-closing call with :for.
And when a component should wrap caller content rather than render a fixed shape, a
slot plus render_slot/1 does it — a
<.panel> that frames whatever you put between its tags.
<%!-- the F6.05.1 list, now one reusable component per row --%> <ul> <.course_card :for={course <- @courses} course={course} /> </ul> # a slot-based wrapper component slot :inner_block, required: true def panel(assigns) do ~H""" <section class="panel">{render_slot(@inner_block)}</section> """ end # call it: <.panel><p>anything here</p></.panel>
render with attrs
<.course_card course={course} /> — typed, reusable, one definition.
wrap with a slot
render_slot/1 frames caller-supplied content inside the component.
Next: forms & inputs — Phoenix
ships <.form> and <.input> as components exactly like
these, backed by a changeset.