查看源代码 表单绑定

表单事件

要处理表单更改和提交,请使用 phx-changephx-submit 事件。通常情况下,建议在表单级别处理输入更改,在该级别,所有表单字段都将传递到 LiveView 的回调,以响应任何单个输入更改。例如,要处理实时表单验证和保存,您的表单将同时使用 phx-changephx-submit 绑定。让我们从一个示例开始

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

.form 是在 Phoenix.Component.form/1 中定义的功能组件,我们建议阅读其文档以详细了解其工作原理和所有支持的选项。 .form 预计有一个 @form 赋值,它可以通过 Phoenix.Component.to_form/1 从变更集或用户参数创建。

input/1 是一个用于渲染输入的功能组件,通常在您的应用程序中定义,通常封装标签、错误处理等。以下是一个简单的入门版本

attr :field, Phoenix.HTML.FormField
attr :rest, :global, include: ~w(type)
def input(assigns) do
  ~H"""
  <input id={@field.id} name={@field.name} value={@field.value} {@rest} />
  """
end

The CoreComponents module

如果您的应用程序是用 Phoenix v1.7 生成的,那么 mix phx.new 会自动导入许多现成的功能组件,例如具有内置功能和样式的 .input 组件。

渲染表单后,您的 LiveView 会在 handle_event 回调中拾取事件,以相应地验证并尝试保存参数

def render(assigns) ...

def mount(_params, _session, socket) do
  {:ok, assign(socket, form: to_form(Accounts.change_user(%User{})))}
end

def handle_event("validate", %{"user" => params}, socket) do
  form =
    %User{}
    |> Accounts.change_user(params)
    |> to_form(action: :validate)

  {:noreply, assign(socket, form: form)}
end

def handle_event("save", %{"user" => user_params}, socket) do
  case Accounts.create_user(user_params) do
    {:ok, user} ->
      {:noreply,
       socket
       |> put_flash(:info, "user created")
       |> redirect(to: ~p"/users/#{user}")}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, form: to_form(changeset))}
  end
end

验证回调只是根据所有表单输入值更新变更集,然后将变更集转换为表单并将其分配给套接字。如果表单发生更改,例如生成新的错误,则会调用 render/1,并且表单将重新渲染。

同样对于 phx-submit 绑定,会调用相同的回调,并尝试进行持久化。成功后,将返回一个 :noreply 元组,并使用 Phoenix.LiveView.redirect/2 为套接字添加注释以重定向到新的用户页面,否则套接字赋值将使用错误的变更集进行更新,以便为客户端重新渲染。

您可能希望单个输入使用自己的更改事件或定位不同的组件。这可以通过在输入本身添加 phx-change 来实现,例如

<.form for={@form} phx-change="validate" phx-submit="save">
  ...
  <.input field={@form[:email]}  phx-change="email_changed" phx-target={@myself} />
</.form>

然后您的 LiveView 或 LiveComponent 将处理该事件

def handle_event("email_changed", %{"user" => %{"email" => email}}, socket) do
  ...
end

注意:对于使用 phx-change 标记的输入,仅发送单个输入作为参数。

错误反馈

为了在表单更新时获得适当的错误反馈,错误标签必须指定它们属于哪个输入。这是通过 phx-feedback-for 实现的。

phx-feedback-for 注释指定了它所属的输入的名称(或 ID,用于向后兼容)。如果没有添加 phx-feedback-for 属性,会导致显示用户尚未更改的表单字段的错误消息(例如,页面下方必需的字段)。

例如,您的 MyAppWeb.CoreComponents 可能会使用此函数

def input(assigns) do
  ~H"""
  <div phx-feedback-for={@name}>
    <input
      type={@type}
      name={@name}
      id={@id || @name}
      value={Phoenix.HTML.Form.normalize_value(@type, @value)}
      class={[
        "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
        "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
      ]}
      {@rest}
    />
    <.error :for={msg <- @errors}><%= msg %></.error>
  </div>
  """
end

def error(assigns) do
  ~H"""
  <p class="phx-no-feedback:hidden">
    <Heroicons.exclamation_circle mini class="mt-0.5 h-5 w-5 flex-none fill-rose-500" />
    <%= render_slot(@inner_block) %>
  </p>
  """
end

现在,任何具有 phx-feedback-for 属性的 DOM 容器,在表单字段尚未收到用户输入/焦点的情况下,将接收一个 phx-no-feedback 类。使用新的 CSS 规则或 tailwindcss 变体,允许您在反馈发生变化时显示、隐藏和设置错误的样式。

数字输入

数字输入是 LiveView 表单中的一个特殊情况。在程序化更新中,某些浏览器将清除无效的输入。因此,当输入无效时,LiveView 不会从客户端发送更改事件,而是允许浏览器的本机验证 UI 驱动用户交互。一旦输入变得有效,更改和提交事件将正常发送。

<input type="number">

这已知存在许多问题,包括可访问性、将大数字转换为指数表示法,以及滚动可能会意外地增加或减少数字。

一个替代方法是 inputmode 属性,它可能更适合您的应用程序需求和用户。根据 Can I Use?,以下方法在全球 86% 的市场(截至 2021 年 9 月)中得到支持

<input type="text" inputmode="numeric" pattern="[0-9]*">

密码输入

密码输入在 Phoenix.HTML 中也是特殊情况。出于安全原因,渲染密码输入标签时不会重复使用密码字段值。这要求您在标记中显式设置 :value,例如

<.input field={f[:password]} value={input_value(f[:password].value)} />
<.input field={f[:password_confirmation]} value={input_value(f[:password_confirmation].value)} />

嵌套输入

嵌套输入是使用 .inputs_for 功能组件处理的。默认情况下,它将添加必要的隐藏输入字段,以跟踪 Ecto 关联的 ID。

<.inputs_for :let={fp} field={f[:friends]}>
  <.input field={fp[:name]} type="text" />
</.inputs_for>

文件输入

LiveView 表单支持 反应式文件输入,包括通过 phx-drop-target 属性进行拖放支持

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

有关更多信息,请参见 Phoenix.Component.live_file_input/1

通过 HTTP 提交表单操作

可以在表单中添加 phx-trigger-action 属性,以在 DOM 修补到表单标准 action 属性中指定的 URL 时触发标准表单提交。这对于在将 LiveView 表单提交发布到控制器路由以进行需要 Plug 会话变异的操作之前,执行最终验证非常有用。例如,在您的 LiveView 模板中,您可以使用布尔赋值为 phx-trigger-action 添加注释

<.form :let={f} for={@changeset}
  action={~p"/users/reset_password"}
  phx-submit="save"
  phx-trigger-action={@trigger_submit}>

然后,在您的 LiveView 中,您可以切换赋值以在下次渲染时触发表单,并使用当前字段。

def handle_event("save", params, socket) do
  case validate_change_password(socket.assigns.user, params) do
    {:ok, changeset} ->
      {:noreply, assign(socket, changeset: changeset, trigger_submit: true)}

    {:error, changeset} ->
      {:noreply, assign(socket, changeset: changeset)}
  end
end

一旦 phx-trigger-action 为 true,LiveView 就会断开连接,然后提交表单。

崩溃或断开连接后的恢复

默认情况下,所有标记有 phx-change 并具有 id 属性的表单,在用户重新连接或 LiveView 在崩溃后重新挂载后,都会自动恢复输入值。这是通过客户端在挂载完成后立即向服务器触发相同的 phx-change 来实现的。

注意:如果您想查看开发中的表单恢复工作,请确保通过在您的 endpoint.ex 文件中注释掉 LiveReload 插件或在您的 config/dev.exs 中设置 code_reloader: false 来禁用开发中的实时重新加载。否则,实时重新加载可能会在您重新启动服务器时导致当前页面重新加载,这将丢弃所有表单状态。

对于大多数用例,这就是您所需要的,表单恢复将在没有考虑的情况下发生。在某些情况下,如果表单以有状态的方式逐步构建,则可能需要在服务器上对现有 phx-change 回调代码之外进行额外的恢复处理。要启用专门的恢复,请在表单上提供一个 phx-auto-recover 绑定,以指定用于恢复的不同事件,该事件将照常接收表单参数。例如,假设一个 LiveView 向导表单,其中表单是有状态的,并且根据用户所在的步骤和之前的选择进行构建

<form id="wizard" phx-change="validate_wizard_step" phx-auto-recover="recover_wizard">

在服务器端,"validate_wizard_step" 事件只关心当前客户端表单数据,但服务器维护着向导的整个状态。为了在此场景中恢复,您可以指定一个恢复事件,例如上面的 "recover_wizard",它将连接到您的 LiveView 中的以下服务器回调

def handle_event("validate_wizard_step", params, socket) do
  # regular validations for current step
  {:noreply, socket}
end

def handle_event("recover_wizard", params, socket) do
  # rebuild state based on client input data up to the current step
  {:noreply, socket}
end

要放弃自动表单恢复,请设置 phx-auto-recover="ignore"

重置表单

要重置 LiveView 表单,可以使用表单按钮或输入上的标准 type="reset"。单击后,表单输入将重置为其原始值。表单重置后,会发出一个 phx-change 事件,其中 _target 参数包含重置的 name。例如,以下元素

<form phx-change="changed">
  ...
  <button type="reset" name="reset">Reset</button>
</form>

可以在服务器上以与常规更改函数不同的方式进行处理

def handle_event("changed", %{"_target" => ["reset"]} = params, socket) do
  # handle form reset
end

def handle_event("changed", params, socket) do
  # handle regular form change
end

JavaScript 客户端详细信息

JavaScript 客户端始终是当前输入值的真相来源。对于任何具有焦点的输入,LiveView 永远不会覆盖输入的当前值,即使它偏离了服务器渲染的更新。这对于预计不会产生重大副作用的更新非常有效,例如表单验证错误,或者在用户填写表单时围绕用户输入值的增量 UX。

对于这些用例,phx-change 输入并不关心在事件发送到服务器时禁用输入编辑。当 phx-change 事件发送到服务器时,输入标签和父表单标签会接收 phx-change-loading CSS 类,然后将有效负载推送到服务器,并在根有效负载中包含一个 "_target" 参数,其中包含触发更改事件的输入名称的键空间。

例如,如果以下输入触发了一个更改事件

<input name="user[username]"/>

服务器的 handle_event/3 将接收一个有效负载

%{"_target" => ["user", "username"], "user" => %{"username" => "Name"}}

phx-submit 事件用于表单提交,在表单提交中通常会发生重大副作用,例如渲染新的容器、调用外部服务或重定向到新页面。

在提交绑定了 phx-submit 事件的表单时

  1. 表单的输入将设置为 readonly
  2. 表单上的任何提交按钮都将被禁用
  3. 表单将接收 "phx-submit-loading"

在完成服务器对 phx-submit 事件的处理后

  1. 提交的表单将被重新激活,并失去 "phx-submit-loading"
  2. 恢复最后一个具有焦点的输入(除非另一个输入获得了焦点)
  3. 更新将照常修补到 DOM

为了处理延迟事件,表单的 <button> 标签可以添加 phx-disable-with 注释,该注释在事件提交期间用提供的 value 交换元素的 innerText。例如,以下代码将把“保存”按钮更改为“正在保存...”,并在确认后将其恢复为“保存”

<button type="submit" phx-disable-with="Saving...">Save</button>

您还可以利用 LiveView 的 CSS 加载状态类在表单提交时交换表单内容。例如,使用 app.css 中的以下规则

.while-submitting { display: none; }
.inputs { display: block; }

.phx-submit-loading .while-submitting { display: block; }
.phx-submit-loading .inputs { display: none; }

您可以使用以下标记显示和隐藏内容

<form phx-change="update">
  <div class="while-submitting">Please wait while we save our content...</div>
  <div class="inputs">
    <input type="text" name="text" value={@text}>
  </div>
</form>

此外,我们强烈建议在表单上包含唯一的 HTML “id” 属性。当 DOM 同级元素发生变化时,没有 ID 的元素将被替换而不是移动,这会导致诸如表单字段失去焦点等问题。

使用 JavaScript 触发 phx- 表单事件

通常情况下,希望在没有显式用户交互的情况下触发 DOM 元素上的事件。例如,自定义表单元素,如日期选择器或自定义选择输入,它使用隐藏的输入元素来存储选择状态。

在这些情况下,可以使用 DOM API 上的事件函数,例如触发 phx-change 事件

document.getElementById("my-select").dispatchEvent(
  new Event("input", {bubbles: true})
)

当使用客户端钩子时,可以使用 this.el 来确定元素,如“客户端钩子”文档中所述。

也可以使用“提交”事件触发 phx-submit 事件

document.getElementById("my-form").dispatchEvent(
  new Event("submit", {bubbles: true, cancelable: true})
)