LiveView Components
Phoenix LiveView function components and live components — attrs, slots, HEEx templates, and stateful components
Components are the building blocks of reusable UI in Phoenix LiveView. There are two types: function components (stateless) and live components (stateful).
Function components
Function components are pure functions that take an assigns map and return HEEx markup:
defmodule MyAppWeb.CoreComponents do
use Phoenix.Component
attr :label, :string, required: true
attr :class, :string, default: nil
attr :rest, :global
def button(assigns) do
~H"""
<button class={["btn", @class]} {@rest}>
<%= @label %>
</button>
"""
end
endUsing a function component
<.button label="Save" phx-click="save" />
<.button label="Cancel" class="btn-secondary" />Attrs
Attrs validate and document component inputs:
attr :name, :string, required: true
attr :age, :integer, default: 0
attr :role, :string, values: ["admin", "editor", "viewer"]
attr :active, :boolean, default: false
attr :items, :list, default: []
attr :user, :map, required: true
attr :class, :string, default: nil
attr :rest, :global # captures remaining HTML attributesAttr types
| Type | Description |
|---|---|
:string |
String value |
:integer |
Integer value |
:float |
Float value |
:boolean |
Boolean value |
:atom |
Atom value |
:list |
List value |
:map |
Map / struct |
:any |
Any type |
Slots
Slots allow passing content blocks into components:
slot :header, doc: "Optional header content"
slot :inner_block, required: true
def card(assigns) do
~H"""
<div class="card">
<div :if={@header != []} class="card-header">
<%= render_slot(@header) %>
</div>
<div class="card-body">
<%= render_slot(@inner_block) %>
</div>
</div>
"""
endUsing slots
<.card>
<:header>Important Notice</:header>
This is the card content.
</.card>Named slots with arguments
attr :items, :list, required: true
slot :item, required: true do
attr :item, :map, required: true
end
def list_items(assigns) do
~H"""
<ul>
<li :for={item <- @items}>
<%= render_slot(@item, item) %>
</li>
</ul>
"""
end<.list_items items={@users}>
<:item :let={item}>
<%= item.name %> — <%= item.email %>
</:item>
</.list_items>Built-in form components
Phoenix generates a CoreComponents module with common components:
.simple_form
<.simple_form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:title]} label="Title" />
<.input field={@form[:body]} type="textarea" label="Body" />
<:actions>
<.button>Save Post</.button>
</:actions>
</.simple_form>.input
<!-- Text input -->
<.input field={@form[:name]} label="Name" />
<!-- Email input -->
<.input field={@form[:email]} type="email" label="Email" />
<!-- Password input -->
<.input field={@form[:password]} type="password" label="Password" />
<!-- Textarea -->
<.input field={@form[:body]} type="textarea" label="Body" />
<!-- Select -->
<.input field={@form[:role]} type="select" options={["Admin", "Editor", "Viewer"]} />
<!-- Checkbox -->
<.input field={@form[:active]} type="checkbox" label="Active?" />Live components (stateful)
Live components maintain their own state and life cycle. Use them for interactive, self-contained pieces of UI:
defmodule MyAppWeb.SortableHeaderComponent do
use Phoenix.LiveComponent
attr :field, :atom, required: true
attr :current_sort, :atom, required: true
attr :direction, :atom, required: true
def render(assigns) do
~H"""
<th class="cursor-pointer" phx-click="sort" phx-value-field={@field} phx-target={@myself}>
<%= humanize(@field) %>
<span :if={@current_sort == @field}>
<%= if @direction == :asc, do: "↑", else: "↓" %>
</span>
</th>
"""
end
endEmbedding a live component
<.live_component module={MyAppWeb.SortableHeaderComponent}
id="sort-name"
field={:name}
current_sort={@sort_field}
direction={@sort_dir} />Live component life cycle
| Callback | Purpose |
|---|---|
mount/1 |
Initialize socket assigns (no params) |
update/2 |
Receive new assigns from parent |
update/3 |
Receive new assigns + params from send_update |
render/1 |
Render the component |
handle_event/3 |
Handle events (use phx-target={@myself}) |
defmodule MyAppWeb.AccordionComponent do
use Phoenix.LiveComponent
@impl true
def mount(socket) do
{:ok, assign(socket, open: false)}
end
@impl true
def update(assigns, socket) do
{:ok, assign(socket, assigns)}
end
@impl true
def handle_event("toggle", _, socket) do
{:noreply, update(socket, :open, &!/1)}
end
@impl true
def render(assigns) do
~H"""
<div>
<button phx-click="toggle" phx-target={@myself}>
<%= if @open, do: "▼", else: "▶" %> <%= @title %>
</button>
<div :if={@open} class="panel">
<%= render_slot(@inner_block) %>
</div>
</div>
"""
end
endImportant: Events in live components must use
phx-target={@myself}so the LiveView routes the event to the component instead of the parent.
send_update
Update a live component from outside its own events:
# From a parent LiveView or another process
Phoenix.LiveView.send_update(MyAppWeb.ModalComponent, id: "confirm-modal", show: true)
# The component receives it in update/3
@impl true
def update(%{show: show}, socket) do
{:ok, assign(socket, show: show)}
endHEEx template features
<!-- Conditional rendering -->
<div :if={@user}>
Hello, <%= @user.name %>
</div>
<!-- Loop -->
<ul>
<li :for={item <- @items}><%= item.name %></li>
</ul>
<!-- Dynamic attributes -->
<div class={["card", @active && "card-active"]} id={@id}>
...
</div>
<!-- Safe HTML interpolation -->
<span>{@raw_html_content}</span>
<!-- Attribute shorthand -->
<input type="text" {@extra_attrs} />
<!-- Dynamic tag -->
<.tag name={@as_header ? "h1" : "p"}>Content</.tag>