You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Phoenix LiveView表单处理validate事件时出现函数子句不匹配错误

Phoenix LiveView表单删除内容后崩溃的原因分析

问题场景

编写的用户注册LiveView表单初始渲染正常,空字段能显示错误提示,但输入内容后删除时表单崩溃,抛出FunctionClauseError。

代码实现

defmodule TodoappWeb.UserRegistrationLive do
  alias Todoapp.Accounts
  use TodoappWeb, :live_view
  alias Todoapp.User

  @impl true
  def render(assigns) do
    ~H"""
    <.form :let={f} id="registration-form" for={@form} phx-submit="save" phx-change="validate">
      <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
        Register
      </h2>
      <.input type="text" field={f[:firstname]} label="Firstname" />

      <.input type="text" field={f[:middlename]} label="Middlename" />
      <.input type="text" field={f[:lastname]} label="Lastname" />

      <.input type="text" field={f[:email]} label="Email" />

      <br />
      <button
        type="submit"
        class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
      >
        Register
      </button>
    </.form>
    <br />
    <.link patch={~p"/users/login"}>Login</.link>
    """
  end

  @impl true
  def mount(_params, _session, socket) do
    form =
      %User{}
      |> User.changeset(%{})
      |> to_form(as: "form")

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

  @impl true
  def handle_event("save", %{"form" => params}, socket) do
    case Accounts.register_user(params) do
      {:ok, _user} ->
        {:noreply,
         socket
         |> put_flash(:info, "User registered successfully!")
         |> redirect(to: ~p"/users/login")}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply,
         socket
         |> put_flash(:error, "Registration failed. Please check the errors below.")
         |> assign(form: to_form(changeset))}
    end
  end

  @impl true
  def handle_event("validate", %{"form" => params}, socket) do
    form =
      %User{}
      |> User.changeset(params)
      |> Map.put(:action, :validate)
      |> to_form(as: "form")

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

错误日志

[error] GenServer #PID<0.5544.0> terminating
** (FunctionClauseError) no function clause matching in TodoappWeb.UserRegistrationLive.handle_event/3
    (todoapp 0.1.0) lib/todoapp_web/live/user_registration_live.ex:44: TodoappWeb.UserRegistrationLive.handle_event("validate", %{"_target" => ["user", "lastname"], "user" => %{"email" => "", "firstname" => "d", "lastname" => "", "middlename" => ""}}, #Phoenix.LiveView.Socket<id: "phx-GAnDLL8TB_56nzlF", endpoint: TodoappWeb.Endpoint, view: TodoappWeb.UserRegistrationLive, parent_pid: nil, root_pid: #PID<0.5544.0>, router: TodoappWeb.Router, assigns: %{form: %Phoenix.HTML.Form{source: #Ecto.Changeset<action: :insert, changes: %{firstname: "d", lastname: "d"}, errors: [email: {"can't be blank", [validation: :required]}], data: #Todoapp.User<>, valid?: false, ...>, impl: Phoenix.HTML.FormData.Ecto.Changeset, id: "user", name: "user", data: %Todoapp.User{__meta__: #Ecto.Schema.Metadata<:built, "users">, id: nil, firstname: nil, middlename: nil, lastname: nil, email: nil, inserted_at: nil, updated_at: nil}, action: :insert, hidden: [], params: %{"email" => "", "firstname" => "d", "lastname" => "d", "middlename" => ""}, errors: [email: {"can't be blank", [validation: :required]}], options: [method: "post"], index: nil}, __changed__: %{}, flash: %{"error" => "Registration failed. Please check the errors below."}, live_action: :new}, transport_pid: #PID<0.5537.0>, ...>)

问题原因

核心问题是表单命名空间不一致:

  • 在mount/3中,创建表单时指定了as: "form",所以初始表单的参数会被包裹在"form"键下;
  • 但在handle_event("save")的错误分支里,调用to_form(changeset)时没有指定as: "form",Ecto默认会使用Schema的名称(即"user",对应Todoapp.User)作为表单的命名空间;
  • 当提交失败后,表单的命名空间变成了"user",此时触发validate事件时,前端传递的参数会被包裹在"user"键下,而你的handle_event("validate")函数只匹配%{"form" => params}的模式,没有对应的函数子句处理%{"user" => params}的情况,因此抛出FunctionClauseError。

修复方法

在save事件的错误分支中,保持表单命名空间一致,指定as: "form":

# 修改handle_event("save")的错误分支
{:error, %Ecto.Changeset{} = changeset} ->
  {:noreply,
   socket
   |> put_flash(:error, "Registration failed. Please check the errors below.")
   |> assign(form: to_form(changeset, as: "form"))} # 这里添加as: "form"

内容的提问来源于stack exchange,提问作者jwesonga

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 05:03:11