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
相关产品推荐
相关产品推荐

