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}>&times;</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