Block-structured DSL for defining function components.
Phoenix.Component's attr and slot macros attach themselves
to the next-defined function via an @on_definition callback.
That's positional — the schema lines must come immediately
before the def, with nothing in between. Interleave anything
and you get weird behaviour; refactoring moves attrs onto the
wrong function silently.
This block makes the function-to-schema relationship explicit:
components do
component :button do
prop :rest, :global, include: ~w(disabled type)
prop :class, :string, default: nil
prop :variant, :atom, values: [:primary, :secondary], default: :primary
slot :inner_block, required: true
render fn assigns ->
~H"""
<button class={["btn", "btn-#{@variant}", @class]} {@rest}>
{render_slot(@inner_block)}
</button>
"""
end
end
component :badge do
prop :label, :string, required: true
prop :count, :integer, default: 0
render fn assigns ->
~H"""
<span class="badge">
<span class="label">{@label}</span>
<span :if={@count > 0} class="count">{@count}</span>
</span>
"""
end
end
endCompiles to plain Phoenix function components — one def
per component clause, with the Phoenix.Component.Declarative
attribute/slot calls emitted immediately above. Consumers
(other templates, other modules) call <.button ...> as
normal — no runtime distinction.
Vocabulary
prop is lavash's term for what Phoenix.Component calls attr.
Identical semantics — prop :foo, :string, default: nil becomes
attr :foo, :string, default: nil in the compiled output. The
reason for the rename: lavash already uses prop for parent-
passed values in LiveComponents (use Lavash.Component).
Standardising on prop across both kinds of component makes
the DSL internally consistent. Coming from Phoenix, "prop = attr"
is the only translation you need.
slot is unchanged — same name, same options as Phoenix.Component.
render fn assigns -> ~H"..." end is the function body. The
lambda takes assigns and returns rendered HEEx. (Same shape as
Phoenix.Component's def name(assigns), do: ~H"...".)
Summary
Functions
A single component :name do ... end clause. The body is a
sequence of prop / slot declarations followed by a single
render fn assigns -> ~H"..." end.
Top-level components do ... end block. Contains one or more
component/2 calls.
Functions
A single component :name do ... end clause. The body is a
sequence of prop / slot declarations followed by a single
render fn assigns -> ~H"..." end.
Top-level components do ... end block. Contains one or more
component/2 calls.