jonnifyelixir
F6 / F6.05 / components

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

attr attr :course, :map, required: true — a typed, checked input compile-checked slot slot :inner_block — a placeholder the caller fills with markup content in render_slot/1 render_slot(@inner_block) — drop the filled content into place content out

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

ATTRS (typed values) course={course} SLOT (markup) inner content course_card(assigns) attrs validated at compile time render_slot fills the hole MARKUP <article> ... </article> rendered into the page attrs are checked; the slot is optional content; the output is plain escaped 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.