LiveView组件 #
一、组件概述 #
1.1 组件类型 #
| 类型 | 说明 |
|---|---|
| 函数组件 | 无状态,纯渲染 |
| LiveComponent | 有状态,独立事件处理 |
1.2 组件优势 #
- 代码复用
- 关注点分离
- 独立状态管理
- 性能优化
二、函数组件 #
2.1 定义函数组件 #
elixir
defmodule HelloWeb.CoreComponents do
use Phoenix.Component
attr :title, :string, required: true
attr :class, :string, default: ""
def card(assigns) do
~H"""
<div class={"card #{@class}"}>
<div class="card-header">
<h3><%= @title %></h3>
</div>
<div class="card-body">
<%= render_slot(@inner_block) %>
</div>
</div>
"""
end
end
2.2 使用函数组件 #
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
2.3 带插槽的组件 #
elixir
attr :title, :string, required: true
slot :header
slot :footer
def panel(assigns) do
~H"""
<div class="panel">
<%= if @header do %>
<div class="panel-header">
<%= render_slot(@header) %>
</div>
<% end %>
<div class="panel-body">
<h2><%= @title %></h2>
<%= render_slot(@inner_block) %>
</div>
<%= if @footer do %>
<div class="panel-footer">
<%= render_slot(@footer) %>
</div>
<% end %>
</div>
"""
end
三、LiveComponent #
3.1 定义LiveComponent #
elixir
defmodule HelloWeb.UserComponent do
use HelloWeb, :live_component
def render(assigns) do
~H"""
<div class="user-card" id={@id}>
<h3><%= @user.name %></h3>
<p><%= @user.email %></p>
<button phx-click="toggle" phx-target={@myself}>
<%= if @expanded, do: "Collapse", else: "Expand" %>
</button>
<%= if @expanded do %>
<div class="user-details">
<p>Age: <%= @user.age %></p>
<p>Bio: <%= @user.bio %></p>
</div>
<% end %>
</div>
"""
end
def mount(socket) do
{:ok, assign(socket, :expanded, false)}
end
def handle_event("toggle", _params, socket) do
{:noreply, update(socket, :expanded, &(!&1))}
end
end
3.2 使用LiveComponent #
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
3.3 组件更新 #
elixir
def update(%{user: user} = assigns, socket) do
{:ok, assign(socket, assigns)}
end
四、组件通信 #
4.1 父组件向子组件传递数据 #
elixir
def render(assigns) do
~H"""
<div>
<%= for user <- @users do %>
<.live_component
module={HelloWeb.UserComponent}
id={user.id}
user={user}
on_delete={&delete_user/1}
/>
<% end %>
</div>
"""
end
4.2 子组件向父组件发送消息 #
elixir
defmodule HelloWeb.UserComponent do
use HelloWeb, :live_component
def handle_event("delete", _params, socket) do
send(self(), {:delete_user, socket.assigns.user.id})
{:noreply, socket}
end
end
父组件处理:
elixir
def handle_info({:delete_user, user_id}, socket) do
{:ok, _} = Hello.Accounts.delete_user(user_id)
{:noreply, assign(socket, :users, Hello.Accounts.list_users())}
end
4.3 使用send_update #
elixir
def handle_event("update_user", %{"id" => id, "name" => name}, socket) do
send_update(HelloWeb.UserComponent, id: id, name: name)
{:noreply, socket}
end
五、表单组件 #
5.1 表单组件定义 #
elixir
defmodule HelloWeb.UserFormComponent do
use HelloWeb, :live_component
def render(assigns) do
~H"""
<div>
<.simple_form :let={f} for={@changeset} phx-target={@myself} phx-change="validate" phx-submit="save">
<.input field={f[:name]} type="text" label="Name" />
<.input field={f[:email]} type="email" label="Email" />
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
</div>
"""
end
def mount(socket) do
{:ok, assign(socket, :changeset, nil)}
end
def update(%{user: user} = assigns, socket) do
changeset = Hello.Accounts.change_user(user)
{:ok, assign(socket, Map.put(assigns, :changeset, changeset))}
end
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
socket.assigns.user
|> Hello.Accounts.change_user(user_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
def handle_event("save", %{"user" => user_params}, socket) do
case Hello.Accounts.update_user(socket.assigns.user, user_params) do
{:ok, user} ->
send(self(), {:user_updated, user})
{:noreply, socket}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
end
5.2 使用表单组件 #
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
六、列表组件 #
6.1 可排序列表 #
elixir
defmodule HelloWeb.SortableListComponent do
use HelloWeb, :live_component
def render(assigns) do
~H"""
<ul id={@id} phx-hook="Sortable">
<%= for item <- @items do %>
<li id={"item-#{item.id}"} data-id={item.id}>
<%= item.name %>
</li>
<% end %>
</ul>
"""
end
def handle_event("reorder", %{"ids" => ids}, socket) do
items = reorder_items(socket.assigns.items, ids)
{:noreply, assign(socket, :items, items)}
end
defp reorder_items(items, ids) do
id_to_item = Map.new(items, fn item -> {Integer.to_string(item.id), item} end)
Enum.map(ids, fn id -> Map.get(id_to_item, id) end)
end
end
6.2 无限滚动 #
elixir
defmodule HelloWeb.InfiniteScrollComponent do
use HelloWeb, :live_component
def render(assigns) do
~H"""
<div id={@id} phx-hook="InfiniteScroll" data-page={@page}>
<%= for item <- @items do %>
<div class="item">
<%= item.name %>
</div>
<% end %>
<%= if @has_more do %>
<div class="loading">Loading more...</div>
<% end %>
</div>
"""
end
def handle_event("load_more", _params, socket) do
page = socket.assigns.page + 1
new_items = Hello.Items.list_items(page: page)
has_more = length(new_items) == socket.assigns.per_page
{:noreply,
socket
|> update(:items, fn items -> items ++ new_items end)
|> assign(:page, page)
|> assign(:has_more, has_more)}
end
end
七、模态框组件 #
7.1 定义模态框 #
elixir
defmodule HelloWeb.ModalComponent do
use HelloWeb, :live_component
def render(assigns) do
~H"""
<div class="modal-overlay" id={@id} phx-remove={hide_modal(@id)}>
<div class="modal-content">
<div class="modal-header">
<h3><%= @title %></h3>
<button phx-click="close" phx-target={@myself}>×</button>
</div>
<div class="modal-body">
<%= render_slot(@inner_block) %>
</div>
</div>
</div>
"""
end
def handle_event("close", _params, socket) do
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
end
defp hide_modal(id) do
"document.getElementById('#{id}').remove()"
end
end
7.2 使用模态框 #
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
八、组件最佳实践 #
8.1 合理划分组件 #
elixir
defmodule HelloWeb.UserLive do
use HelloWeb, :live_view
def render(assigns) do
~H"""
<div>
<.header title="Users" />
<.user_list users={@users} />
<.pagination current={@page} total={@total_pages} />
</div>
"""
end
end
8.2 组件状态最小化 #
elixir
defmodule HelloWeb.UserCard do
use Phoenix.Component
attr :user, :map, required: true
def card(assigns) do
~H"""
<div class="user-card">
<h3><%= @user.name %></h3>
<p><%= @user.email %></p>
</div>
"""
end
end
8.3 使用ID优化更新 #
elixir
def render(assigns) do
~H"""
<ul id="user-list">
<%= for user <- @users do %>
<li id={"user-#{user.id}"}>
<%= user.name %>
</li>
<% end %>
</ul>
"""
end
九、总结 #
9.1 组件类型 #
| 类型 | 说明 | 使用场景 |
|---|---|---|
| 函数组件 | 无状态 | 简单展示 |
| LiveComponent | 有状态 | 复杂交互 |
9.2 核心概念 #
| 概念 | 说明 |
|---|---|
| attr | 属性定义 |
| slot | 插槽 |
| render_slot | 渲染插槽 |
| handle_event | 事件处理 |
| send_update | 更新组件 |
9.3 下一步 #
现在你已经了解了LiveView组件,接下来让我们学习 LiveView表单,深入了解表单处理!
最后更新:2026-03-28