查看源代码 Phoenix.Component (Phoenix LiveView v0.20.17)

使用 HEEx 模板定义可复用的函数组件。

函数组件是指任何接收 assigns 映射作为参数并返回使用 ~H 标识符 构建的渲染结构体的函数。

defmodule MyComponent do
  # In Phoenix apps, the line is typically: use MyAppWeb, :html
  use Phoenix.Component

  def greet(assigns) do
    ~H"""
    <p>Hello, <%= @name %>!</p>
    """
  end
end

此函数使用 ~H 标识符返回渲染后的模板。 ~H 代表 HEEx(HTML + EEx)。 HEEx 是一种用于编写混合了 Elixir 插值的 HTML 的模板语言。 我们可以使用 <%= ... %> 标签在 HEEx 中编写 Elixir 代码,并使用 @name 访问在 assigns 中定义的键 name

~H 标识符或 HEEx 模板文件中调用时

<MyComponent.greet name="Jane" />

将渲染以下 HTML

<p>Hello, Jane!</p>

如果函数组件在本地定义,或者其模块已导入,那么调用者可以直接调用函数,无需指定模块。

<.greet name="Jane" />

对于动态值,您可以将 Elixir 表达式插入函数组件。

<.greet name={@user.name} />

函数组件还可以接受 HEEx 内容块(稍后详细介绍)。

<.card>
  <p>This is the body of my card!</p>
</.card>

在本模块中,我们将学习如何构建丰富的、可组合的组件,以在我们的应用程序中使用。

属性

Phoenix.Component 提供了 attr/3 宏来声明在调用时,后续函数组件期望接收哪些属性。

attr :name, :string, required: true

def greet(assigns) do
  ~H"""
  <p>Hello, <%= @name %>!</p>
  """
end

通过调用 attr/3,现在明确了 greet/1 需要一个名为 name 的字符串属性,该属性存在于其 assigns 映射中,以便正确渲染。 如果没有这样做,将导致编译警告。

<MyComponent.greet />
  <!-- warning: missing required attribute "name" for component MyAppWeb.MyComponent.greet/1
           lib/app_web/my_component.ex:15 -->

属性可以提供默认值,这些默认值会自动合并到 assigns 映射中。

attr :name, :string, default: "Bob"

现在您可以调用函数组件,而无需为 name 提供值。

<.greet />

渲染以下 HTML

<p>Hello, Bob!</p>

访问必需且没有默认值的属性将失败。 您必须显式声明 default: nil 或使用 assign_new/3 函数以编程方式分配值。

可以为同一个函数组件声明多个属性。

attr :name, :string, required: true
attr :age, :integer, required: true

def celebrate(assigns) do
  ~H"""
  <p>
    Happy birthday <%= @name %>!
    You are <%= @age %> years old.
  </p>
  """
end

允许调用者传递多个值。

<.celebrate name={"Genevieve"} age={34} />

渲染以下 HTML

<p>
  Happy birthday Genevieve!
  You are 34 years old.
</p>

可以在同一个模块中定义多个函数组件,它们具有不同的属性。 在以下示例中, <Components.greet/> 需要一个 name,但不需要 title,而 <Components.heading> 需要一个 title,但不需要 name

defmodule Components do
  # In Phoenix apps, the line is typically: use MyAppWeb, :html
  use Phoenix.Component

  attr :title, :string, required: true

  def heading(assigns) do
    ~H"""
    <h1><%= @title %></h1>
    """
  end

  attr :name, :string, required: true

  def greet(assigns) do
    ~H"""
    <p>Hello <%= @name %></p>
    """
  end
end

使用 attr/3 宏,您拥有创建可复用函数组件的核心要素。 但如果您需要您的函数组件支持动态属性,例如混合到组件容器中的常用 HTML 属性,该怎么办?

全局属性

全局属性是函数组件在声明类型为 :global 的属性时可以接受的一组属性。 默认情况下,接受的属性集是所有标准 HTML 标签共有的属性。 有关属性的完整列表,请参阅 全局属性

声明全局属性后,调用者可以传递任意数量的属性集中的属性,而无需修改函数组件本身。

下面是一个接受动态数量全局属性的函数组件示例。

attr :message, :string, required: true
attr :rest, :global

def notification(assigns) do
  ~H"""
  <span {@rest}><%= @message %></span>
  """
end

调用者可以传递多个全局属性(例如 phx-* 绑定或 class 属性)。

<.notification message="You've got mail!" class="bg-green-200" phx-click="close" />

渲染以下 HTML

<span class="bg-green-200" phx-click="close">You've got mail!</span>

请注意,函数组件不必显式声明 classphx-click 属性即可进行渲染。

全局属性可以定义默认值,这些默认值将与调用者提供的属性合并。 例如,如果您未提供 class 属性,则可以声明一个默认 class

attr :rest, :global, default: %{class: "bg-blue-200"}

现在您可以调用函数组件,而无需 class 属性。

<.notification message="You've got mail!" phx-click="close" />

渲染以下 HTML

<span class="bg-blue-200" phx-click="close">You've got mail!</span>

请注意,不能直接提供全局属性,这样做会导致发出警告。 换句话说,以下操作无效。

<.notification message="You've got mail!" rest={%{"phx-click" => "close"}} />

包含的全局属性

您还可以使用 :include 选项指定除已知全局属性之外还包括哪些属性。 例如,要在按钮组件上支持 form 属性。

# <.button form="my-form"/>
attr :rest, :global, include: ~w(form)
slot :inner_block
def button(assigns) do
  ~H"""
  <button {@rest}><%= render_slot(@inner_block) %></button>
  """
end

:include 选项非常适合在个别情况下应用全局添加,但有时您希望使用新的全局属性扩展现有组件,例如 Alpine.js 的 x- 前缀,我们将在下面概述这些属性。

自定义全局属性前缀

您可以通过向 use Phoenix.Component 提供属性前缀列表来扩展全局属性集。 与所有 HTML 元素共有的默认属性一样,以全局前缀开头的任意数量的属性都将被当前模块调用的函数组件接受。 默认情况下,支持以下前缀: phx-aria-data-。 例如,要支持 Alpine.js 使用的 x- 前缀,您可以将 :global_prefixes 选项传递给 use Phoenix.Component

use Phoenix.Component, global_prefixes: ~w(x-)

在您的 Phoenix 应用程序中,这通常是在您的 lib/my_app_web.ex 文件中完成的,位于 def html 定义中。

def html do
  quote do
    use Phoenix.Component, global_prefixes: ~w(x-)
    # ...
  end
end

现在,此模块调用的所有函数组件都将接受以 x- 为前缀的任意数量的属性,以及默认的全局前缀。

您可以通过阅读 attr/3 的文档来了解有关属性的更多信息。

插槽

除了属性外,函数组件还可以接受 HEEx 内容块,称为插槽。 插槽使渲染的 HTML 能够进一步定制,因为调用者可以向函数组件传递他们希望组件渲染的 HEEx 内容。 Phoenix.Component 提供了 slot/3 宏,用于为函数组件声明插槽。

slot :inner_block, required: true

def button(assigns) do
  ~H"""
  <button>
    <%= render_slot(@inner_block) %>
  </button>
  """
end

表达式 render_slot(@inner_block) 渲染 HEEx 内容。 您可以像这样调用此函数组件。

<.button>
  This renders <strong>inside</strong> the button!
</.button>

这将渲染以下 HTML。

<button>
  This renders <strong>inside</strong> the button!
</button>

attr/3 宏类似,使用 slot/3 宏将提供编译时验证。 例如,调用 button/1 而不带 HEEx 内容的插槽将导致发出编译警告。

<.button />
  <!-- warning: missing required slot "inner_block" for component MyAppWeb.MyComponent.button/1
           lib/app_web/my_component.ex:15 -->

默认插槽

上面的示例使用默认插槽(可作为名为 @inner_block 的分配访问),通过 render_slot/1 函数渲染 HEEx 内容。

如果插槽中渲染的值需要是动态的,则可以通过调用 render_slot/2 将第二个值传递回 HEEx 内容。

slot :inner_block, required: true

attr :entries, :list, default: []

def unordered_list(assigns) do
  ~H"""
  <ul>
    <%= for entry <- @entries do %>
      <li><%= render_slot(@inner_block, entry) %></li>
    <% end %>
  </ul>
  """
end

调用函数组件时,可以使用特殊属性 :let 来获取函数组件传递回来的值并将其绑定到变量。

<.unordered_list :let={fruit} entries={~w(apples bananas cherries)}>
  I like <b><%= fruit %></b>!
</.unordered_list>

渲染以下 HTML

<ul>
  <li>I like <b>apples</b>!</li>
  <li>I like <b>bananas</b>!</li>
  <li>I like <b>cherries</b>!</li>
</ul>

现在,关注点分离得以保持:调用者可以在列表属性中指定多个值,而无需指定围绕和分隔它们的 HEEx 内容。

命名插槽

除了默认插槽外,函数组件还可以接受多个命名的 HEEx 内容插槽。 例如,假设您要创建一个包含标题、主体和页脚的模态框。

slot :header
slot :inner_block, required: true
slot :footer, required: true

def modal(assigns) do
  ~H"""
  <div class="modal">
    <div class="modal-header">
      <%= render_slot(@header) || "Modal" %>
    </div>
    <div class="modal-body">
      <%= render_slot(@inner_block) %>
    </div>
    <div class="modal-footer">
      <%= render_slot(@footer) %>
    </div>
  </div>
  """
end

您可以使用命名的插槽 HEEx 语法调用此函数组件。

<.modal>
  This is the body, everything not in a named slot is rendered in the default slot.
  <:footer>
    This is the bottom of the modal.
  </:footer>
</.modal>

渲染以下 HTML

<div class="modal">
  <div class="modal-header">
    Modal.
  </div>
  <div class="modal-body">
    This is the body, everything not in a named slot is rendered in the default slot.
  </div>
  <div class="modal-footer">
    This is the bottom of the modal.
  </div>
</div>

如上例所示,当声明可选插槽且未提供任何插槽时, render_slot/1 返回 nil。 这可用于附加默认行为。

插槽属性

与默认插槽不同,可以向命名插槽传递多段 HEEx 内容。 命名插槽还可以接受属性,这些属性通过向 slot/3 宏传递代码块来定义。 如果传递了多段内容, render_slot/2 将合并并渲染所有值。

下面是一个说明具有属性的多个命名插槽的表格组件。

slot :column, doc: "Columns with column labels" do
  attr :label, :string, required: true, doc: "Column label"
end

attr :rows, :list, default: []

def table(assigns) do
  ~H"""
  <table>
    <tr>
      <%= for col <- @column do %>
        <th><%= col.label %></th>
      <% end %>
    </tr>
    <%= for row <- @rows do %>
      <tr>
        <%= for col <- @column do %>
          <td><%= render_slot(col, row) %></td>
        <% end %>
      </tr>
    <% end %>
  </table>
  """
end

您可以像这样调用此函数组件。

<.table rows={[%{name: "Jane", age: "34"}, %{name: "Bob", age: "51"}]}>
  <:column :let={user} label="Name">
    <%= user.name %>
  </:column>
  <:column :let={user} label="Age">
    <%= user.age %>
  </:column>
</.table>

渲染以下 HTML

<table>
  <tr>
    <th>Name</th>
    <th>Age</th>
  </tr>
  <tr>
    <td>Jane</td>
    <td>34</td>
  </tr>
  <tr>
    <td>Bob</td>
    <td>51</td>
  </tr>
</table>

您可以了解有关插槽和 slot/3 宏的更多信息 在其文档中

嵌入外部模板文件

embed_templates/1 宏可用于将 .html.heex 文件嵌入为函数组件。 目录路径基于当前模块 (__DIR__),可以使用通配符模式选择目录树中的所有文件。 例如,想象一个目录列表。

 components.ex
 cards
    pricing_card.html.heex
    features_card.html.heex

然后,您可以在 components.ex 模块中嵌入页面模板,并像调用任何其他函数组件一样调用它们。

defmodule MyAppWeb.Components do
  use Phoenix.Component

  embed_templates "cards/*"

  def landing_hero(assigns) do
    ~H"""
    <.pricing_card />
    <.features_card />
    """
  end
end

有关更多信息,包括嵌入模板的声明式分配支持,请参阅 embed_templates/1

调试注释

HEEx 模板支持调试注释,这些注释是特殊的 HTML 注释,它们围绕渲染的组件进行包装,以帮助您识别 HTML 文档中的标记在函数组件树中的渲染位置。

例如,想象以下 HEEx 模板。

<.header>
  <.button>Click</.button>
</.header>

当启用调试注释时,HTML 文档将接收以下注释。

<!-- @caller lib/app_web/home_live.ex:20 -->
<!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->
<header class="p-5">
  <!-- @caller lib/app_web/home_live.ex:48 -->
  <!-- <AppWeb.CoreComponents.button> lib/app_web/core_components.ex:456 -->
  <button class="px-2 bg-indigo-500 text-white">Click</button>
  <!-- </AppWeb.CoreComponents.button> -->
</header>
<!-- </AppWeb.CoreComponents.header> -->

调试注释适用于任何 ~H.html.heex 模板。 它们可以通过以下配置在您的 config/dev.exs 文件中全局启用。

config :phoenix_live_view, debug_heex_annotations: true

更改此配置需要 mix clean 和完全重新编译。

摘要

组件

使用不同加载状态的插槽渲染异步分配。 result 状态优先于后续加载状态和失败状态。

生成动态命名的 HTML 标签。

围绕容器包装选项卡焦点,以实现可访问性。

渲染表单。

为关联或嵌入渲染嵌套的表单输入。

在可枚举类型之间插入分隔符插槽。

生成指向给定路由的链接。

一个函数组件,用于在父 LiveView 中渲染 Phoenix.LiveComponent

为 LiveView 上传构建文件输入标签。

为选定的文件在客户端生成图像预览。

@page_title 更新时渲染标题,并自动添加前缀/后缀。

为 HEEx 函数组件声明属性。

将外部模板文件作为函数组件嵌入到模块中。

在源文件中编写 HEEx 模板的 ~H 标记。

声明一个插槽。有关更多信息,请参阅 slot/3

声明一个函数组件插槽。

函数

将键值对添加到分配中。

key-value 对添加到 socket_or_assigns 中。

如果不存在,则使用 fun 中的值将给定的 key 分配到 socket_or_assigns 中。

将分配作为关键字列表进行过滤,以用于动态标签属性。

检查给定的键是否在 socket_or_assigns 中更改。

从 LiveView 闪存分配中返回闪存消息。

在模板中渲染 LiveView。

使用给定的可选 argument 渲染插槽条目。

将给定的数据结构转换为 Phoenix.HTML.Form

在给定的 socket_or_assigns 中使用 fun 更新现有的 key

返回整个上传的错误。

返回上传条目的错误。

组件

使用不同加载状态的插槽渲染异步分配。 result 状态优先于后续加载状态和失败状态。

注意:内部块接收异步分配的结果作为 :let。let 只能访问内部块,不能访问其他插槽。

示例

<.async_result :let={org} assign={@org}>
  <:loading>Loading organization...</:loading>
  <:failed :let={_failure}>there was an error loading the organization</:failed>
  <%= if org do %>
    <%= org.name %>
  <% else %>
    You don't have an organization yet.
  <% end %>
</.async_result>

要在后续 assign_async 调用中再次显示加载和失败状态,请将分配重置为无结果的 %AsyncResult{}

{:noreply,
  socket
  |> assign_async(:page, :data, &reload_data/0)
  |> assign(:page, AsyncResult.loading())}

属性

插槽

  • loading - 在分配首次加载时渲染。
  • failed - 在捕获错误或退出或 assign_async 首次返回 {:error, reason} 时渲染。接收错误作为 :let
  • inner_block - 当分配通过 AsyncResult.ok/2 成功加载时渲染。接收结果作为 :let

生成动态命名的 HTML 标签。

如果发现标签名称是不安全的 HTML,则会引发 ArgumentError

属性

  • name (:string) (必需) - 标签名称,例如 div
  • 接受全局属性。要添加到标签中的其他 HTML 属性,确保正确转义。

插槽

  • inner_block

示例

<.dynamic_tag name="input" type="text"/>
<input type="text"/>
<.dynamic_tag name="p">content</.dynamic_tag>
<p>content</p>

围绕容器包装选项卡焦点,以实现可访问性。

这是模态框、对话框和菜单等界面必不可少的辅助功能。

属性

  • id (:string) (必需) - 容器标签的 DOM 标识符。
  • 接受全局属性。要添加到容器标签中的其他 HTML 属性。

插槽

  • inner_block (必需) - 渲染在容器标签内的内容。

示例

只需在此组件内渲染您的内部内容,当用户在容器内容中切换选项卡时,焦点将围绕容器进行包装。

<.focus_wrap id="my-modal" class="bg-white">
  <div id="modal-content">
    Are you sure?
    <button phx-click="cancel">Cancel</button>
    <button phx-click="confirm">OK</button>
  </div>
</.focus_wrap>

渲染表单。

此函数接收一个 Phoenix.HTML.Form 结构,通常使用 to_form/2 创建,并生成相关的表单标签。它可以在 LiveView 内或外部使用。

要查看表单在实践中的工作方式,您可以在 Phoenix 应用程序中运行 mix phx.gen.live Blog Post posts title body:text,这将设置必要的数据库表和 LiveView 来管理您的数据。

示例:在 LiveView 内

在 LiveView 内,此函数组件通常使用 for={@form} 调用,其中 @formto_form/1 函数的结果。 to_form/1 预期数据源为映射或 Ecto.Changeset,并将其规范化为 Phoenix.HTML.Form 结构。

例如,您可以使用在 Phoenix.LiveView.handle_event/3 回调中接收的参数来创建 Ecto changeset,然后使用 to_form/1 将其转换为表单。然后,在您的模板中,将 @form 作为参数传递给 :for

<.form
  for={@form}
  phx-change="change_name"
>
  <.input field={@form[:email]} />
</.form>

.input 组件通常定义为您的应用程序的一部分,并添加所有必要的样式。

def input(assigns) do
  ~H"""
  <input type="text" name={@field.name} id={@field.id} value={@field.value} class="..." />
  """
end

表单接受多个选项。例如,如果您正在进行文件上传,并且您想捕获提交,您可以改写为

<.form
  for={@form}
  multipart
  phx-change="change_user"
  phx-submit="save_user"
>
  ...
  <input type="submit" value="Save" />
</.form>

请注意,两个示例都使用 phx-change。LiveView 必须实现 phx-change 事件,并在输入值到达时存储它们。这很重要,因为如果页面上发生了无关的更改,LiveView 应该使用更新的值重新渲染输入。如果没有 phx-change,输入将被清除。或者,您可以在表单上使用 phx-update="ignore" 来丢弃任何更新。

使用 for 属性

for 属性也可以是映射或 Ecto.Changeset。在这种情况下,将动态创建表单,您可以使用 :let 捕获它。

<.form
  :let={form}
  for={@changeset}
  phx-change="change_user"
>

但是,这种方法在 LiveView 中不建议使用,原因有两个

  • 如果您使用 @form[:field] 而不是通过 let 变量 form 来访问表单字段,LiveView 可以更好地优化您的代码。

  • Ecto changeset 旨在一次性使用。通过从未将 changeset 存储在分配中,您将不太可能在操作之间使用它。

关于 :errors 的说明

即使 changeset.errors 不为空,如果 changeset 的 :actionnil:ignore,则不会在表单中显示错误。

这对表单字段的验证提示很有用,例如新表单的空 changeset。该 changeset 无效,但我们不想在执行实际用户操作之前显示错误。

例如,如果用户提交并调用 Repo.insert/1,并且在 changeset 验证时失败,则操作将设置为 :insert 以表明尝试插入,并且该操作的存在将导致显示错误。对于 Repo.update/delete 也是如此。

如果您想手动显示错误,您也可以自己设置操作,直接在 Ecto.Changeset 结构字段上设置,或者使用 Ecto.Changeset.apply_action/2 设置。由于操作可以是任意的,您可以将其设置为 :validate 或任何其他内容,以避免给人的印象是实际上已经尝试过数据库操作。

示例:在 LiveView 外部(常规 HTTP 请求)

form 组件仍然可以用来在 LiveView 之外提交表单。在这种情况下,必须给出 action 属性。如果没有该属性,则会丢弃 form 方法和 csrf 令牌。

<.form :let={f} for={@changeset} action={~p"/comments/#{@comment}"}>
  <.input field={f[:body]} />
</.form>

在上面的示例中,我们将 changeset 传递给 for,并使用 :let={f} 捕获了该值。这种方法在 LiveView 外部是可以的,因为没有更改跟踪优化需要考虑。

CSRF 防护

CSRF 防护是一种机制,用于确保渲染表单的用户是实际提交表单的用户。默认情况下,此模块会生成一个 CSRF 令牌。您的应用程序应该在服务器上检查此令牌,以防止攻击者代表其他用户在您的服务器上发出请求。Phoenix 默认情况下会检查此令牌。

在将表单发布到其地址中包含主机的主机时,例如 "//host.com/path" 而不是只使用 "/path",Phoenix 会将主机签名包含在令牌中,并且仅在访问的主机与令牌中的主机相同的情况下验证令牌。这是为了防止令牌泄露到第三方应用程序。如果此行为有问题,您可以使用 Plug.CSRFProtection.get_csrf_token/0 生成一个非主机特定的令牌,并通过 :csrf_token 选项将其传递给表单生成器。

属性

  • for (:any) (必填) - 现有表单或表单源数据。

  • action (:string) - 提交表单时要执行的操作。如果您打算将表单提交到没有 LiveView 的 URL,则必须提供此属性。

  • as (:atom) - 表单生成的名称和 ID 中要使用的前缀。例如,设置 as: :user_params 表示参数将在您的 handle_event 中嵌套 "user_params",或者对于常规 HTTP 请求,将嵌套在 conn.params["user_params"] 中。如果您设置了此选项,则必须使用 :let 捕获表单。

  • csrf_token (:any) - 用于验证请求有效性的令牌。当提供操作且方法不是 get 时,会自动生成一个。设置为 false 时,不会生成任何令牌。

  • errors (:list) - 使用此选项可手动将错误的关键字列表传递给表单。当将常规映射作为表单源提供时,此选项很有用,它将使错误在 f.errors 下可用。如果您设置了此选项,则必须使用 :let 捕获表单。

  • method (:string) - HTTP 方法。仅在提供 :action 时使用。如果方法不是 get 也不 post,则会在表单标记旁边生成一个带有名称 _method 的输入标记。如果提供 :action 但没有方法,则方法将默认为 post

  • multipart (:boolean) - 将 enctype 设置为 multipart/form-data。上传文件时需要此选项。

    默认为 false

  • 接受全局属性。要添加到表单标记的其他 HTML 属性。支持所有全局属性以及:["autocomplete", "name", "rel", "enctype", "novalidate", "target"]

插槽

  • inner_block (必填) - 在表单标记内渲染的内容。

为关联或嵌入渲染嵌套的表单输入。

属性

  • field (Phoenix.HTML.FormField) (必填) - 一个 %Phoenix.HTML.Form{}/field 名称元组,例如:{@form[:email]}。

  • id (:string) - 要在表单中使用的 ID,默认为给定字段与父表单 ID 的连接。

  • as (:atom) - 要在表单中使用的名称,默认为给定字段与父表单名称的连接。

  • default (:any) - 如果没有可用值,则要使用的值。

  • prepend (:list) - 渲染时要预先添加的值。这仅适用于字段值为列表且没有通过表单发送参数的情况。

  • append (:list) - 渲染时要追加的值。这仅适用于字段值为列表且没有通过表单发送参数的情况。

  • skip_hidden (:boolean) - 跳过自动渲染隐藏字段,以便更严格地控制生成的标记。

    默认为 false

  • options (:list) - Phoenix.HTML.FormData 协议实现的任何其他选项。

    默认为 []

插槽

  • inner_block (必填) - 为每个嵌套表单渲染的内容。

示例

<.form
  :let={f}
  phx-change="change_name"
>
  <.inputs_for :let={f_nested} field={f[:nested]}>
    <.input type="text" field={f_nested[:name]} />
  </.inputs_for>
</.form>

动态添加和删除输入

动态添加和删除输入可以通过为插入和删除渲染命名按钮来支持。与输入一样,具有名称/值对的按钮在更改和提交事件时会使用表单数据进行序列化。然后,诸如 Ecto 之类的库或自定义参数过滤可以检查参数并处理添加或删除的字段。这可以与 Ecto.Changeset.cast/3:sort_param:drop_param 选项结合使用。例如,假设一个父级具有 :emails has_manyembeds_many 关联。要将来自嵌套表单的用户输入强制转换为类型,只需要配置选项即可

schema "mailing_lists" do
  field :title, :string

  embeds_many :emails, EmailNotification, on_replace: :delete do
    field :email, :string
    field :name, :string
  end
end

def changeset(list, attrs) do
  list
  |> cast(attrs, [:title])
  |> cast_embed(:emails,
    with: &email_changeset/2,
    sort_param: :emails_sort,
    drop_param: :emails_drop
  )
end

这里我们可以看到 :sort_param:drop_param 选项的作用。

注意:当使用这些选项时,需要在 has_manyembeds_many 上使用 on_replace: :delete

当 Ecto 从表单中看到指定的排序或删除参数时,它将根据子项在表单中出现的顺序对子项进行排序,添加它没有见过的新的子项,或者如果参数指示这样做,则删除子项。

此类架构和关联的标记将如下所示

<.inputs_for :let={ef} field={@form[:emails]}>
  <input type="hidden" name="mailing_list[emails_sort][]" value={ef.index} />
  <.input type="text" field={ef[:email]} placeholder="email" />
  <.input type="text" field={ef[:name]} placeholder="name" />
  <button
    type="button"
    name="mailing_list[emails_drop][]"
    value={ef.index}
    phx-click={JS.dispatch("change")}
  >
    <.icon name="hero-x-mark" class="w-6 h-6 relative top-2" />
  </button>
</.inputs_for>

<input type="hidden" name="mailing_list[emails_drop][]" />

<button type="button" name="mailing_list[emails_sort][]" value="new" phx-click={JS.dispatch("change")}>
  add more
</button>

我们使用 inputs_for 来渲染 :emails 关联的输入,该关联包含每个子项的电子邮件地址和名称输入。在嵌套输入中,我们渲染一个隐藏的 mailing_list[emails_sort][] 输入,该输入设置为给定子项的索引。这告诉 Ecto 的强制转换操作如何对现有子项进行排序,或者在哪里插入新的子项。接下来,我们像往常一样渲染电子邮件和名称输入。然后,我们渲染一个包含 "delete" 文本的按钮,按钮名称为 mailing_list[emails_drop][],其中包含子项索引作为其值。

与之前一样,这告诉 Ecto 在单击按钮时删除此索引处的子项。我们在按钮上使用 phx-click={JS.dispatch("change")} 告诉 LiveView 将此按钮单击视为更改事件,而不是表单上的提交事件,这会调用我们表单的 phx-change 绑定。

inputs_for 之外,我们渲染一个空的 mailing_list[emails_drop][] 输入,以确保在保存用户删除所有条目的表单时删除所有子项。当删除关联时,需要此隐藏输入。

最后,我们还渲染另一个带有排序参数名称 mailing_list[emails_sort][]value="new" 名称的按钮,以及伴随的 "add more" 文本。请注意,此按钮必须具有 type="button",以防止它提交表单。Ecto 将将未知排序参数视为新的子项并构建一个新的子项。此按钮是可选的,仅在您需要动态添加条目时才需要。您可以在 <.inputs_for> 之前选择性地添加一个类似的按钮,以防您需要在前面添加条目。

在可枚举类型之间插入分隔符插槽。

当您需要在项目之间添加分隔符时很有用,例如在渲染导航的面包屑时。将每个项目提供给内部块。

示例

<.intersperse :let={item} enum={["home", "profile", "settings"]}>
  <:separator>
    <span class="sep">|</span>
  </:separator>
  <%= item %>
</.intersperse>

渲染以下标记

home <span class="sep">|</span> profile <span class="sep">|</span> settings

属性

  • enum (:any) (必填) - 要用分隔符进行插入的可枚举对象。

插槽

  • inner_block (必填) - 为每个项目渲染的内部块。
  • separator (必填) - 分隔符的插槽。

生成指向给定路由的链接。

要使用传统的浏览器导航在页面之间导航,请使用 href 属性。要修补当前 LiveView 或在 LiveView 之间导航,请分别使用 patchnavigate

属性

  • navigate (:string) - 从 LiveView 导航到新的 LiveView。浏览器页面保持不变,但会挂载一个新的 LiveView 进程,并且页面上的其内容会重新加载。只能在同一个路由器 Phoenix.LiveView.Router.live_session/3 下声明的 LiveView 之间导航。否则,将使用完整的浏览器重定向。

  • patch (:string) - 修补当前 LiveView。将调用当前 LiveView 的 handle_params 回调,并且会发送最少的内容,就像任何其他 LiveView 差异一样。

  • href (:any) - 使用传统的浏览器导航到新位置。这意味着浏览器上的整个页面都将重新加载。

  • replace (:boolean) - 当使用 :patch:navigate 时,是否应该使用 pushState 替换浏览器的历史记录?

    默认为 false

  • method (:string) - 要与链接一起使用的 HTTP 方法。这是为了在 LiveView 之外使用而设计的,因此仅适用于 href={...} 属性。它对 patchnavigate 指令没有影响。

    如果方法不是 get,则链接将在设置正确信息的表单中生成。为了提交表单,浏览器中必须启用 JavaScript。

    默认为 "get"

  • csrf_token (:any) - 用于 HTTP 方法不为 get 的链接的布尔值或自定义令牌。默认为 true

  • 接受全局属性。添加到 a 标记的其他 HTML 属性。支持所有全局属性以及:["download", "hreflang", "referrerpolicy", "rel", "target", "type"]

插槽

  • inner_block (必填) - 在 a 标记内渲染的内容。

示例

<.link href="/">Regular anchor link</.link>
<.link navigate={~p"/"} class="underline">home</.link>
<.link navigate={~p"/?sort=asc"} replace={false}>
  Sort By Price
</.link>
<.link patch={~p"/details"}>view details</.link>
<.link href={URI.parse("https://elixir.erlang.ac.cn")}>hello</.link>
<.link href="/the_world" method="delete" data-confirm="Really?">delete</.link>

JavaScript 依赖项

为了支持 :method 不为 "get" 的链接或使用上述数据属性,Phoenix.HTML 依赖于 JavaScript。您可以将 priv/static/phoenix_html.js 加载到构建工具中。

数据属性

数据属性作为传递给 data 键的关键字列表添加。支持以下数据属性

  • data-confirm - 当 :method 不为 "get" 时,在生成并提交表单之前显示确认提示。

覆盖默认确认行为

phoenix_html.js 在发生点击时确实会在被点击的 DOM 元素上触发自定义事件 phoenix.link.click。这使您可以拦截事件在向 window 冒泡的路上,并执行您自己的自定义逻辑来增强或替换 data-confirm 属性的处理方式。例如,您可以用自定义 JavaScript 实现替换浏览器的 confirm() 行为。

// Compared to a javascript window.confirm, the custom dialog does not block
// javascript execution. Therefore to make this work as expected we store
// the successful confirmation as an attribute and re-trigger the click event.
// On the second click, the `data-confirm-resolved` attribute is set and we proceed.
const RESOLVED_ATTRIBUTE = "data-confirm-resolved";
// listen on document.body, so it's executed before the default of
// phoenix_html, which is listening on the window object
document.body.addEventListener('phoenix.link.click', function (e) {
  // Prevent default implementation
  e.stopPropagation();
  // Introduce alternative implementation
  var message = e.target.getAttribute("data-confirm");
  if(!message){ return; }

  // Confirm is resolved execute the click event
  if (e.target?.hasAttribute(RESOLVED_ATTRIBUTE)) {
    e.target.removeAttribute(RESOLVED_ATTRIBUTE);
    return;
  }

  // Confirm is needed, preventDefault and show your modal
  e.preventDefault();
  e.target?.setAttribute(RESOLVED_ATTRIBUTE, "");

  vex.dialog.confirm({
    message: message,
    callback: function (value) {
      if (value == true) {
        // Customer confirmed, re-trigger the click event.
        e.target?.click();
      } else {
        // Customer canceled
        e.target?.removeAttribute(RESOLVED_ATTRIBUTE);
      }
    }
  })
}, false);

或者,您可以附加自己的自定义行为。

window.addEventListener('phoenix.link.click', function (e) {
  // Introduce custom behaviour
  var message = e.target.getAttribute("data-prompt");
  var answer = e.target.getAttribute("data-prompt-answer");
  if(message && answer && (answer != window.prompt(message))) {
    e.preventDefault();
  }
}, false);

后者也可以绑定到任何 click 事件,但这可以确保您的自定义代码仅在运行 phoenix_html.js 的代码时才会执行。

CSRF 防护

默认情况下,CSRF 令牌是通过 Plug.CSRFProtection 生成的。

链接到此函数

live_component(assigns)

查看源代码

一个函数组件,用于在父 LiveView 中渲染 Phoenix.LiveComponent

虽然 LiveView 可以嵌套,但每个 LiveView 都启动了自己的进程。LiveComponent 提供与 LiveView 相似的功能,只是它们在与 LiveView 相同的进程中运行,并拥有自己的封装状态。这就是为什么它们被称为有状态组件。

属性

  • id (:string) (required) - LiveComponent 的唯一标识符。请注意,id 不一定会用作 DOM id。这取决于组件自行决定。

  • module (:atom) (required) - 要渲染的 LiveComponent 模块。

提供的任何其他属性都将作为分配映射传递给 LiveComponent。有关更多信息,请参阅 Phoenix.LiveComponent

示例

<.live_component module={MyApp.WeatherComponent} id="thermostat" city="Kraków" />
链接到此函数

live_file_input(assigns)

查看源代码

为 LiveView 上传构建文件输入标签。

属性

  • upload (Phoenix.LiveView.UploadConfig) (required) - Phoenix.LiveView.UploadConfig 结构体。
  • accept (:string) - accept 属性的可选覆盖。默认为 allow_upload 指定的 :accept。
  • 接受全局属性。支持所有全局属性以及:["webkitdirectory", "required", "disabled", "capture", "form"]

拖放

通过使用指向 UploadConfig refphx-drop-target 属性注释可拖放容器来支持拖放,因此以下标记是支持拖放所需的所有内容。

<div class="container" phx-drop-target={@uploads.avatar.ref}>
  <!-- ... -->
  <.live_file_input upload={@uploads.avatar} />
</div>

示例

渲染文件输入

<.live_file_input upload={@uploads.avatar} />

渲染带有标签的文件输入

<label for={@uploads.avatar.ref}>Avatar</label>
<.live_file_input upload={@uploads.avatar} />
链接到此函数

live_img_preview(assigns)

查看源代码

为选定的文件在客户端生成图像预览。

属性

  • entry (Phoenix.LiveView.UploadEntry) (required) - Phoenix.LiveView.UploadEntry 结构体。
  • id (:string) - img 标签的 id。默认情况下,从条目引用派生,但如果需要在同一页面上多次渲染同一条目的预览,则可以根据需要覆盖。默认为 nil
  • 接受全局属性。

示例

<%= for entry <- @uploads.avatar.entries do %>
  <.live_img_preview entry={entry} width="75" />
<% end %>

当您需要多次使用它时,请确保它们具有不同的 id。

<%= for entry <- @uploads.avatar.entries do %>
  <.live_img_preview entry={entry} width="75" />
<% end %>

<%= for entry <- @uploads.avatar.entries do %>
  <.live_img_preview id={"modal-#{entry.ref}"} entry={entry} width="500" />
<% end %>

@page_title 更新时渲染标题,并自动添加前缀/后缀。

属性

  • prefix (:string) - 在 inner_block 的内容之前添加的前缀。默认为 nil
  • suffix (:string) - 在 inner_block 的内容之后添加的后缀。默认为 nil

插槽

  • inner_block (required) - 在 title 标签内渲染的内容。

示例

<.live_title prefix="MyApp  ">
  <%= assigns[:page_title] || "Welcome" %>
</.live_title>
<.live_title suffix="- MyApp">
  <%= assigns[:page_title] || "Welcome" %>
</.live_title>

指向此宏的链接

attr(name, type, opts \\ [])

查看源代码 (宏)

为 HEEx 函数组件声明属性。

参数

  • name - 定义属性名称的原子。请注意,属性不能定义与同一组件声明的任何其他属性或插槽相同的名称。

  • type - 定义属性类型的原子。

  • opts - 选项的关键字列表。默认为 []

类型

属性由其名称、类型和选项声明。支持以下类型

名称描述
:any任何项
:string任何二进制字符串
:atom任何原子(包括 truefalsenil
:boolean任何布尔值
:integer任何整数
:float任何浮点数
:list任何任意类型的列表
:map任何任意类型的映射
:global任何常见的 HTML 属性,以及由 :global_prefixes 定义的属性
结构体模块任何使用 defstruct/1 定义结构体的模块

选项

  • :required - 将属性标记为必需。如果调用方未传递给定属性,则会发出编译警告。

  • :default - 如果未提供,则为属性的默认值。如果未设置此选项且未给出属性,则访问属性将失败,除非使用 assign_new/3 显式设置值。

  • :examples - 属性接受的值的非详尽列表,用于文档目的。

  • :values - 属性接受的值的详尽列表。如果调用方传递的文字不包含在此列表中,则会发出编译警告。

  • :doc - 属性的文档。

编译时验证

LiveView 通过 :phoenix_live_view 编译器对属性进行一些验证。定义属性时,如果

  • 组件的必需属性缺失。

  • 给出了未知属性。

  • 您指定了文字属性(例如 value="string"value,但不是 value={expr})且类型不匹配。以下类型当前支持文字验证::string:atom:boolean:integer:float:map:list

  • 您指定了文字属性,但它不是 :values 列表的成员。

LiveView 不执行任何运行时验证。这意味着类型信息主要用于文档和反射目的。

在 LiveView 组件本身方面,定义属性提供了以下生活质量改进

  • 所有属性的默认值将预先添加到 assigns 映射中。

  • 为组件生成属性文档。

  • 注释必需的结构体类型并发出编译警告。例如,如果您指定 attr :user, User, required: true,然后在模板中编写 @user.non_valid_field,则会发出警告。

  • 跟踪对组件的调用以进行反射和验证目的。

文档生成

定义属性的公共函数组件将根据 @doc 模块属性的值将其属性类型和文档注入函数的文档中

  • 如果 @doc 是字符串,则属性文档将注入该字符串。可选占位符 [INSERT LVATTRDOCS] 可用于指定在字符串中的哪个位置注入文档。否则,文档将追加到 @doc 字符串的末尾。

  • 如果未指定 @doc,则属性文档将用作默认的 @doc 字符串。

  • 如果 @docfalse,则完全省略属性文档。

注入的属性文档将格式化为 Markdown 列表

  • name (:type) (required) - 属性文档。默认为 :default

默认情况下,所有属性的类型和文档都将注入函数 @doc 字符串中。要隐藏特定属性,可以将 :doc 的值设置为 false

示例

attr :name, :string, required: true
attr :age, :integer, required: true

def celebrate(assigns) do
  ~H"""
  <p>
    Happy birthday <%= @name %>!
    You are <%= @age %> years old.
  </p>
  """
end
指向此宏的链接

embed_templates(pattern, opts \\ [])

查看源代码 (宏)

将外部模板文件作为函数组件嵌入到模块中。

选项

  • :root - 要嵌入文件的根目录。默认为当前模块的目录 (__DIR__)
  • :suffix - 要追加到嵌入函数名称的字符串值。默认情况下,函数名称将是模板文件名,不包括格式和引擎。

可以使用通配符模式来选择目录树中的所有文件。例如,想象一个目录列表

 components.ex
 pages
    about_page.html.heex
    welcome_page.html.heex

然后在您的 components.ex 模块中嵌入页面模板

defmodule MyAppWeb.Components do
  use Phoenix.Component

  embed_templates "pages/*"
end

现在,您的模块将定义一个 about_page/1welcome_page/1 函数组件。嵌入模板还支持通过无体函数定义声明分配,例如

defmodule MyAppWeb.Components do
  use Phoenix.Component

  embed_templates "pages/*"

  attr :name, :string, required: true
  def welcome_page(assigns)

  slot :header
  def about_page(assigns)
end

还支持多次调用 embed_templates,如果您有多种模板格式,这将非常有用。例如

defmodule MyAppWeb.Emails do
  use Phoenix.Component

  embed_templates "emails/*.html", suffix: "_html"
  embed_templates "emails/*.text", suffix: "_text"
end

注意:此函数与 Phoenix.Template.embed_templates/2 相同。它也提供在此处用于方便和文档目的。因此,如果您想要为其他格式(与 Phoenix.Component 无关)嵌入模板,请优先使用 import Phoenix.Template, only: [embed_templates: 1],而不是此模块。

指向此宏的链接

sigil_H(arg, list)

View Source (宏)

在源文件中编写 HEEx 模板的 ~H 标记。

HEEx 是 Elixir 嵌入式语言 (EEx) 的一个支持 HTML 和组件的扩展,它提供了

  • 内置的 HTML 属性处理

  • 用于注入函数组件的类 HTML 符号

  • 模板结构的编译时验证

  • 能够最大限度地减少通过网络发送的数据量

  • 通过 mix format 实现开箱即用的代码格式化

示例

~H"""
<div title="My div" class={@class}>
  <p>Hello <%= @name %></p>
  <MyApp.Weather.city name="Kraków"/>
</div>
"""

语法

HEEx 建立在嵌入式 Elixir (EEx) 之上。在本节中,我们将介绍 HEEx 模板中的基本结构及其语法扩展。

插值

HEExEEx 模板都使用 <%= ... %> 在 HTML 标签主体中插值代码

<p>Hello, <%= @name %></p>

同样,也支持条件语句和其他块级 Elixir 结构

<%= if @show_greeting? do %>
  <p>Hello, <%= @name %></p>
<% end %>

请注意,我们没有在结束标签 <% end %> 中包含等号 =(因为结束标签不会输出任何内容)。

HEEx 和 Elixir 的内置 EEx 之间有一个重要区别。 HEEx 使用特定的注解来插值 HTML 标签和属性。让我们来看看。

HEEx 扩展:定义属性

由于 HEEx 必须解析和验证 HTML 结构,因此使用 <%= ... %><% ... %> 的代码插值仅限于 HTML/组件节点的主体(内部内容),不能应用于标签内。

例如,以下语法是无效的

<div class="<%= @class %>">
  ...
</div>

而应该这样写

<div class={@class}>
  ...
</div>

您可以在 { ... } 中放置任何 Elixir 表达式。例如,如果您想设置类,其中一些是静态的,另一些是动态的,您可以使用字符串插值

<div class={"btn btn-#{@type}"}>
  ...
</div>

以下属性值具有特殊含义

  • true - 如果一个值为 true,则该属性将被渲染,没有任何值。例如,<input required={true}> 等同于 <input required>

  • falsenil - 如果一个值为 falsenil,则该属性将被忽略。为了优化,某些属性可能会被渲染成空值,如果它与忽略具有相同效果。例如,<checkbox checked={false}> 渲染为 <checkbox>,而 <div class={false}> 渲染为 <div class="">

  • list(仅适用于 class 属性) - 列表中的每个元素都将被处理为一个不同的类。 nilfalse 元素将被丢弃。

对于多个动态属性,您可以使用相同的符号,但不将表达式分配给任何特定的属性。

<div {@dynamic_attrs}>
  ...
</div>

{...} 内的表达式必须是一个关键字列表或一个映射,其中包含表示动态属性的键值对。

HEEx 扩展:定义函数组件

函数组件是无状态组件,使用 Phoenix.Component 模块作为纯函数实现。它们可以是局部的(同一模块)或远程的(外部模块)。

HEEx 允许使用类 HTML 符号直接在模板中调用这些函数组件。例如,一个远程函数

<MyApp.Weather.city name="Kraków"/>

一个局部函数可以使用一个前导点调用

<.city name="Kraków"/>

其中组件的定义如下

defmodule MyApp.Weather do
  use Phoenix.Component

  def city(assigns) do
    ~H"""
    The chosen city is: <%= @name %>.
    """
  end

  def country(assigns) do
    ~H"""
    The chosen country is: <%= @name %>.
    """
  end
end

通常最好将相关函数组合到一个模块中,而不是拥有多个只有一个 render/1 函数的模块。函数组件支持其他重要的功能,例如插槽。您可以在 Phoenix.Component 中了解有关组件的更多信息。

HEEx 扩展:特殊属性

除了普通的 HTML 属性外,HEEx 还支持一些特殊属性,例如 :let:for

:let

这被组件和插槽用于将值传回给调用方。例如,请参阅 form/1 的工作方式

<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
  <.input field={f[:username]} type="text" />
  ...
</.form>

请注意 .form 定义的变量 f 如何被您的 input 组件使用。 Phoenix.Component 模块详细记录了如何使用和实现此类功能。

:if 和 :for

这是 <%= if .. do %><%= for .. do %> 的语法糖,可以在普通的 HTML、函数组件和插槽中使用。

例如,在 HTML 标签中

<table id="admin-table" :if={@admin?}>
  <tr :for={user <- @users}>
    <td><%= user.name %></td>
  </tr>
<table>

上面的代码段只有在 @admin? 为 true 时才会渲染表格,并根据用户生成一个 tr,正如您对集合的期望一样。

:for 可以在函数组件中以类似的方式使用

<.error :for={msg <- @errors} message={msg}/>

这等同于编写

<%= for msg <- @errors do %>
  <.error message={msg} />
<% end %>

而在插槽中 :for 的行为方式相同

<.table id="my-table" rows={@users}>
  <:col :for={header <- @headers} :let={user}>
    <td><%= user[header] %></td>
  </:col>
<table>

您还可以将 :for:if 结合起来,使标签、组件和插槽充当过滤器

<.error :for={msg <- @errors} :if={msg != nil} message={msg} />

请注意,与 Elixir 的常规 for 不同,HEEx 的 :for 不支持在一个表达式中使用多个生成器。

代码格式化

您可以使用 Phoenix.LiveView.HTMLFormatter 自动格式化 HEEx 模板 (.heex) 和 ~H 符号。请查看该模块以获取更多信息。

指向此宏的链接

slot(name, opts \\ [])

View Source (宏)

声明一个插槽。有关更多信息,请参阅 slot/3

指向此宏的链接

slot(name, opts, block)

View Source (宏)

声明一个函数组件插槽。

参数

  • name - 一个原子,定义插槽的名称。请注意,插槽不能定义与同一个组件声明的任何其他插槽或属性相同的名称。

  • opts - 选项的关键字列表。默认为 []

  • block - 一个包含对 attr/3 的调用的代码块。默认为 nil

选项

  • :required - 将插槽标记为必需的。如果调用方没有为必需的插槽传递值,则会发出编译警告。否则,省略的插槽将默认为 []

  • :validate_attrs - 当设置为 false 时,如果调用方将属性传递给没有 do 块的插槽,则不会发出警告。如果没有设置,则默认为 true

  • :doc - 插槽的文档。声明的所有插槽属性的文档都将列在插槽旁边。

插槽属性

命名插槽可以通过传递包含对 attr/3 的调用的块来声明属性。

与属性不同,插槽属性不能接受 :default 选项。传递一个会导致发出编译警告。

默认插槽

默认插槽可以通过将 :inner_block 作为插槽的 name 来声明。

请注意,:inner_block 插槽声明不能接受块。传递一个会导致编译错误。

编译时验证

LiveView 通过 :phoenix_live_view 编译器对插槽进行一些验证。当定义插槽时,如果

  • 组件的必需插槽缺失。

  • 给出了未知的插槽。

  • 给出了未知的插槽属性。

在函数组件本身方面,定义属性提供了以下质量改进

  • 为组件生成插槽文档。

  • 跟踪对组件的调用以进行反射和验证目的。

文档生成

定义插槽的公共函数组件将根据 @doc 模块属性的值将其文档注入函数的文档中

  • 如果 @doc 是一个字符串,则插槽文档将被注入该字符串中。可选占位符 [INSERT LVATTRDOCS] 可用于指定在字符串中插入文档的位置。否则,文档将附加到 @doc 字符串的末尾。

  • 如果未指定 @doc,则插槽文档将用作默认的 @doc 字符串。

  • 如果 @docfalse,则插槽文档将完全省略。

注入的插槽文档将被格式化为一个 markdown 列表

  • name (required) - 插槽文档。接受属性
    • name (:type) (required) - 属性文档。默认为 :default

默认情况下,所有插槽的文档都将被注入函数 @doc 字符串中。要隐藏特定的插槽,可以将 :doc 的值设置为 false

示例

slot :header
slot :inner_block, required: true
slot :footer

def modal(assigns) do
  ~H"""
  <div class="modal">
    <div class="modal-header">
      <%= render_slot(@header) || "Modal" %>
    </div>
    <div class="modal-body">
      <%= render_slot(@inner_block) %>
    </div>
    <div class="modal-footer">
      <%= render_slot(@footer) || submit_button() %>
    </div>
  </div>
  """
end

如上例所示,当声明可选插槽且未提供任何插槽时, render_slot/1 返回 nil。 这可用于附加默认行为。

函数

链接到此函数

assign(socket_or_assigns, keyword_or_map)

查看源代码

将键值对添加到分配中。

第一个参数是 LiveView socket 或函数组件中的 assigns 映射。

必须以关键字列表或映射的形式给出 assigns,以便将其合并到现有的 assigns 中。

示例

iex> assign(socket, name: "Elixir", logo: "💧")
iex> assign(socket, %{name: "Elixir"})
链接到此函数

assign(socket_or_assigns, key, value)

查看源代码

key-value 对添加到 socket_or_assigns 中。

第一个参数是 LiveView socket 或函数组件中的 assigns 映射。

示例

iex> assign(socket, :name, "Elixir")
链接到此函数

assign_new(socket_or_assigns, key, fun)

查看源代码

如果不存在,则使用 fun 中的值将给定的 key 分配到 socket_or_assigns 中。

第一个参数是 LiveView socket 或函数组件中的 assigns 映射。

此函数对于延迟分配值和共享 assigns 非常有用。我们将在接下来介绍这两种用例。

延迟 assigns

假设您有一个接受颜色的函数组件

<.my_component bg_color="red" />

颜色也是可选的,因此您可以跳过它

<.my_component />

在这种情况下,实现可以使用 assign_new 来延迟分配颜色,如果没有给出颜色的话。让我们把它改成,如果没有给出颜色,就选择一个随机的颜色

def my_component(assigns) do
  assigns = assign_new(assigns, :bg_color, fn -> Enum.random(~w(bg-red-200 bg-green-200 bg-blue-200)) end)

  ~H"""
  <div class={@bg_color}>
    Example
  </div>
  """
end

共享分配

可以在断开渲染时在 Plug 管道和 LiveView 之间共享分配,以及在连接时在父-子 LiveView 之间共享分配。

断开连接时

当用户首次使用 LiveView 访问应用程序时,LiveView 会首先以断开连接状态进行渲染,作为常规 HTML 响应的一部分。通过在 LiveView 的挂载回调中使用 assign_new,可以指示 LiveView 在断开连接状态期间重新使用在 conn 中设置的任何分配。

假设有一个执行以下操作的 Plug

# A plug
def authenticate(conn, _opts) do
  if user_id = get_session(conn, :user_id) do
    assign(conn, :current_user, Accounts.get_user!(user_id))
  else
    send_resp(conn, :forbidden)
  end
end

可以在初始渲染期间在 LiveView 中重新使用 :current_user 分配

def mount(_params, %{"user_id" => user_id}, socket) do
  {:ok, assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)}
end

在这种情况下,如果存在 conn.assigns.current_user,则会使用它。如果不存在这样的 :current_user 分配,或者 LiveView 是作为实时导航的一部分挂载的(没有调用任何 Plug 管道),那么会调用匿名函数来执行查询。

连接时

LiveView 还可以通过 assign_new 与子 LiveView 共享分配,只要子 LiveView 在父 LiveView 挂载时也已挂载。让我们看一个例子。

如果父 LiveView 定义了 :current_user 分配,并且子 LiveView 也使用 assign_new/3 在其 mount/3 回调中获取 :current_user,就像上一小节中一样,分配将从父 LiveView 获取,再次避免额外的数据库查询。

请注意,fun 还提供了对先前分配的值的访问。

assigns =
  assigns
  |> assign_new(:foo, fn -> "foo" end)
  |> assign_new(:bar, fn %{foo: foo} -> foo <> "bar" end)

分配共享将在可能的情况下执行,但并非保证。因此,必须确保传递给 assign_new/3 的函数的结果与从父级获取的值相同。否则,请考虑将值作为子 LiveView 的一部分传递给子 LiveView 会话。

链接到此函数

assigns_to_attributes(assigns, exclude \\ [])

查看源代码

将分配作为关键字列表进行过滤,以用于动态标签属性。

应优先使用声明式分配和 :global 属性,而不是此函数。

示例

想象一下以下 my_link 组件,它允许调用者传递 new_window 分配,以及他们想要添加到元素中的任何其他属性,例如类、数据属性等

<.my_link to="/" id={@id} new_window={true} class="my-class">Home</.my_link>

可以使用以下组件来支持动态属性

def my_link(assigns) do
  target = if assigns[:new_window], do: "_blank", else: false
  extra = assigns_to_attributes(assigns, [:new_window, :to])

  assigns =
    assigns
    |> assign(:target, target)
    |> assign(:extra, extra)

  ~H"""
  <a href={@to} target={@target} {@extra}>
    <%= render_slot(@inner_block) %>
  </a>
  """
end

以上将导致以下渲染的 HTML

<a href="/" target="_blank" id="1" class="my-class">Home</a>

assigns_to_attributes 的第二个参数(可选)是用于排除的键列表。它通常包括组件本身保留的键,这些键要么不属于标记,要么已被组件显式处理。

链接到此函数

changed?(socket_or_assigns, key)

查看源代码

检查给定的键是否在 socket_or_assigns 中更改。

第一个参数是 LiveView socket 或函数组件中的 assigns 映射。

示例

iex> changed?(socket, :count)
链接到此函数

live_flash(other, key)

查看源代码
此函数已弃用。在 Phoenix v1.7+ 中使用 Phoenix.Flash.get/2。

从 LiveView 闪存分配中返回闪存消息。

示例

<p class="alert alert-info"><%= live_flash(@flash, :info) %></p>
<p class="alert alert-danger"><%= live_flash(@flash, :error) %></p>
链接到此函数

live_render(conn_or_socket, view, opts \\ [])

查看源代码

在模板中渲染 LiveView。

这在两种情况下很有用

  • 在 LiveView 内部渲染子 LiveView 时。

  • 在常规(非实时)控制器/视图中渲染 LiveView 时。

选项

  • :session - 一个包含二进制键的映射,其中包含要序列化并发送到客户端的额外会话数据。连接中当前的所有会话数据在 LiveView 中都是自动可用的。可以使用此选项提供额外数据。请记住,所有会话数据都会被序列化并发送到客户端,因此应始终将会话中的数据量保持在最低限度。例如,不要存储 User 结构,而应存储“user_id”并在 LiveView 挂载时加载 User。

  • :container - 用于 LiveView 容器的 HTML 标签和 DOM 属性的可选元组。例如:{:li, style: "color: blue;"}。默认情况下,它使用模块定义的容器。有关更多信息,请参阅下面的“容器”部分。

  • :id - 用于唯一标识 LiveView 的 DOM ID 和 ID。在渲染根 LiveView 时会自动生成 :id,但在渲染子 LiveView 时,它是必需的选项。

  • :sticky - 一个可选标志,用于在实时重定向中维护 LiveView,即使它嵌套在另一个 LiveView 中也是如此。如果在实时布局中渲染粘性视图,请确保粘性视图本身不使用相同的布局。可以通过从挂载返回 {:ok, socket, layout: false} 来实现。

示例

从控制器/视图渲染时,可以调用

<%= live_render(@conn, MyApp.ThermostatLive) %>

或者

<%= live_render(@conn, MyApp.ThermostatLive, session: %{"home_id" => @home.id}) %>

在另一个 LiveView 中,必须传递 :id 选项

<%= live_render(@socket, MyApp.ThermostatLive, id: "thermostat") %>

容器

渲染 LiveView 时,其内容将包含在容器中。默认情况下,容器是一个带有一些 LiveView 特定属性的 div 标签。

可以使用不同的方式自定义容器

  • 可以在 use Phoenix.LiveView 上更改默认的 container

    use Phoenix.LiveView, container: {:tr, id: "foo-bar"}
  • 可以在调用 live_render 时覆盖容器标签并传递额外属性(以及在路由器中调用 live 时)。

    live_render socket, MyLiveView, container: {:tr, class: "highlight"}

如果不希望容器影响布局,可以使用 CSS 属性 display: contents 或应用它的类,例如 Tailwind 的 .contents

如果将此设置为 :body,请注意,一旦 LiveView 连接,注入到 body 中的任何内容(例如 Phoenix.LiveReload 功能)都会被丢弃。

指向此宏的链接

render_slot(slot, argument \\ nil)

View Source (宏)

使用给定的可选 argument 渲染插槽条目。

<%= render_slot(@inner_block, @form) %>

如果插槽没有条目,则返回 nil。

如果为同一个插槽定义了多个插槽条目,render_slot/2 会自动渲染所有条目,合并它们的内容。如果要使用条目的属性,需要遍历列表以单独访问每个插槽。

例如,假设有一个表格组件

<.table rows={@users}>
  <:col :let={user} label="Name">
    <%= user.name %>
  </:col>

  <:col :let={user} label="Address">
    <%= user.address %>
  </:col>
</.table>

在顶层,将行作为分配传递,并为要添加到表格中的每一列定义一个 :col 插槽。每列还带有一个 label,它将在表头中使用。

在组件内部,可以使用表头、行和列渲染表格

def table(assigns) do
  ~H"""
  <table>
    <tr>
      <%= for col <- @col do %>
        <th><%= col.label %></th>
      <% end %>
    </tr>
    <%= for row <- @rows do %>
      <tr>
        <%= for col <- @col do %>
          <td><%= render_slot(col, row) %></td>
        <% end %>
      </tr>
    <% end %>
  </table>
  """
end
链接到此函数

to_form(data_or_params, options \\ [])

查看源代码

将给定的数据结构转换为 Phoenix.HTML.Form

这通常用于将映射或 Ecto changeset 转换为要传递给 form/1 组件的表单。

从参数创建表单

如果要基于 handle_event 参数创建表单,可以执行以下操作

def handle_event("submitted", params, socket) do
  {:noreply, assign(socket, form: to_form(params))}
end

将映射传递给 to_form/1 时,它假定该映射包含表单参数,这些参数预计具有字符串键。

还可以指定一个名称来嵌套参数

def handle_event("submitted", %{"user" => user_params}, socket) do
  {:noreply, assign(socket, form: to_form(user_params, as: :user))}
end

从 changeset 创建表单

使用 changeset 时,将从它检索底层数据、表单参数和错误。:as 选项也会自动计算。例如,如果有一个用户模式

defmodule MyApp.Users.User do
  use Ecto.Schema

  schema "..." do
    ...
  end
end

然后创建一个 changeset 并将其传递给 to_form

%MyApp.Users.User{}
|> Ecto.Changeset.change()
|> to_form()

在这种情况下,一旦提交表单,参数将在 %{"user" => user_params} 下可用。

选项

  • :as - 用于表单输入的 name 前缀
  • :id - 用于表单输入的 id 前缀
  • :errors - 错误的关键字列表(仅映射使用)

底层数据在转换为表单时可能接受其他选项。例如,映射接受 :errors 来列出错误,但 changeset 不接受此选项。:errors 是一个关键字,包含形式为 {error_message, options_list} 的元组。以下是一个示例

to_form(%{"search" => nil}, errors: [search: {"Can't be blank", []}])

如果给定现有的 Phoenix.HTML.Form 结构,则上面给定的选项将覆盖其现有值。然后将剩余的选项与现有的表单选项合并。

只有当 changeset 的 action 字段设置(并且未设置为 :ignore)时,才会显示表单中的错误。有关更多信息,请参阅关于 :errors 的说明

链接到此函数

update(socket_or_assigns, key, fun)

查看源代码

在给定的 socket_or_assigns 中使用 fun 更新现有的 key

第一个参数是 LiveView socket 或函数组件中的 assigns 映射。

更新函数接收当前键的值,并返回更新后的值。如果键不存在,则引发错误。

更新函数也可以是 2 元的,在这种情况下,它会将当前键的值作为第一个参数接收,并将当前分配作为第二个参数接收。如果键不存在,则引发错误。

示例

iex> update(socket, :count, fn count -> count + 1 end)
iex> update(socket, :count, &(&1 + 1))
iex> update(socket, :max_users_this_session, fn current_max, %{users: users} ->
...>   max(current_max, length(users))
...> end)

返回整个上传的错误。

对于适用于特定上传条目的错误,请使用 upload_errors/2

输出是一个列表。可能会返回以下错误

  • :too_many_files - 选定的文件数量超过了 :max_entries 约束

示例

def upload_error_to_string(:too_many_files), do: "You have selected too many files"
<div :for={err <- upload_errors(@uploads.avatar)} class="alert alert-danger">
  <%= upload_error_to_string(err) %>
</div>
链接到此函数

upload_errors(conf, entry)

查看源代码

返回上传条目的错误。

对于适用于整个上传的错误,请使用 upload_errors/1

输出是一个列表。可能会返回以下错误

  • :too_large - 条目超过了 :max_file_size 约束
  • :not_accepted - 条目与 :accept MIME 类型不匹配
  • :external_client_failure - 当外部上传失败时
  • {:writer_failure, reason} - 当自定义写入器因 reason 失败时

示例

defp upload_error_to_string(:too_large), do: "The file is too large"
defp upload_error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
defp upload_error_to_string(:external_client_failure), do: "Something went terribly wrong"
<%= for entry <- @uploads.avatar.entries do %>
  <div :for={err <- upload_errors(@uploads.avatar, entry)} class="alert alert-danger">
    <%= upload_error_to_string(err) %>
  </div>
<% end %>