如何在Phoenix LiveView中构建多步骤表单?
当然可以在LiveView里实现多步骤表单,而且逻辑比前端框架更简洁——毕竟状态全在服务端管控,不用操心前后端同步的问题。结合嵌入式Schema的场景,按下面的方案来就行:
多步骤表单实现方案
1. 定义嵌入式Schema与分步验证的变化集
先把user和user_profile定义为嵌入式Schema,再用一个主Schema(比如Registration)包裹两者,统一管理表单数据。同时针对不同步骤编写对应的验证规则:
defmodule MyApp.Accounts.User do use Ecto.Schema import Ecto.Changeset embedded_schema do field :email, :string end def changeset(user, attrs) do user |> cast(attrs, [:email]) |> validate_required([:email]) |> validate_format(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/, message: "格式不正确") end end defmodule MyApp.Accounts.UserProfile do use Ecto.Schema import Ecto.Changeset embedded_schema do field :phone_number, :string field :password, :string, virtual: true field :confirm_password, :string, virtual: true end def changeset(profile, attrs) do profile |> cast(attrs, [:phone_number, :password, :confirm_password]) |> validate_required([:phone_number, :password, :confirm_password]) |> validate_length(:password, min: 6) |> validate_confirmation(:password, message: "密码不匹配") |> validate_format(:phone_number, ~r/^1[3-9]\d{9}$/, message: "手机号格式不正确") end end defmodule MyApp.Accounts.Registration do use Ecto.Schema import Ecto.Changeset embedded_schema do embeds_one :user, MyApp.Accounts.User embeds_one :user_profile, MyApp.Accounts.UserProfile end # 步骤1变化集:仅验证邮箱 def step1_changeset(registration, attrs) do registration |> cast(attrs, []) |> cast_embed(:user, with: &MyApp.Accounts.User.changeset/2) |> validate_required([:user]) end # 步骤2变化集:验证手机号、密码,保留已通过的邮箱数据 def step2_changeset(registration, attrs) do registration |> cast(attrs, []) |> cast_embed(:user_profile, with: &MyApp.Accounts.UserProfile.changeset/2) |> validate_required([:user_profile]) end # 最终提交的完整验证 def full_changeset(registration, attrs) do registration |> step1_changeset(attrs) |> step2_changeset(attrs) end end
2. LiveView状态管理与事件处理
在LiveView里维护当前步骤和表单状态,通过事件切换步骤、验证数据,最后完成提交:
defmodule MyAppWeb.RegistrationLive do use MyAppWeb, :live_view alias MyApp.Accounts.Registration @impl true def mount(_params, _session, socket) do registration = %Registration{} form = to_form(Registration.step1_changeset(registration, %{})) socket = assign(socket, current_step: 1, registration: registration, form: form ) {:ok, socket} end @impl true def render(assigns) do ~H""" <div class="registration-form"> <h1>账号注册</h1> <%= if @current_step == 1 do %> <.form for={@form} phx-submit="next-step" class="step-form"> <div class="form-group"> <%= label @form, :email, "邮箱" %> <%= email_input @form, :email, class: "form-control" %> <%= error_tag @form, :email %> </div> <%= submit "下一步", class: "btn btn-primary" %> </.form> <% elsif @current_step == 2 do %> <.form for={@form} phx-submit="submit-registration" class="step-form"> <div class="form-group"> <%= label @form, :"user_profile.phone_number", "手机号" %> <%= text_input @form, :"user_profile.phone_number", class: "form-control" %> <%= error_tag @form, :"user_profile.phone_number" %> </div> <div class="form-group"> <%= label @form, :"user_profile.password", "密码" %> <%= password_input @form, :"user_profile.password", class: "form-control" %> <%= error_tag @form, :"user_profile.password" %> </div> <div class="form-group"> <%= label @form, :"user_profile.confirm_password", "确认密码" %> <%= password_input @form, :"user_profile.confirm_password", class: "form-control" %> <%= error_tag @form, :"user_profile.confirm_password" %> </div> <button type="button" phx-click="prev-step" class="btn btn-secondary">上一步</button> <%= submit "完成注册", class: "btn btn-primary" %> </.form> <% end %> </div> """ end @impl true def handle_event("next-step", params, socket) do %{"registration" => attrs} = params changeset = Registration.step1_changeset(socket.assigns.registration, attrs) if changeset.valid? do form = to_form(Registration.step2_changeset(changeset.data, %{})) socket = assign(socket, current_step: 2, registration: changeset.data, form: form ) {:noreply, socket} else {:noreply, assign(socket, form: to_form(changeset))} end end @impl true def handle_event("prev-step", _params, socket) do form = to_form(Registration.step1_changeset(socket.assigns.registration, %{})) {:noreply, assign(socket, current_step: 1, form: form)} end @impl true def handle_event("submit-registration", params, socket) do %{"registration" => attrs} = params changeset = Registration.full_changeset(socket.assigns.registration, attrs) if changeset.valid? do # 这里写实际的保存逻辑,比如拆分数据存入user和user_profile表 # MyApp.Accounts.create_user(changeset.data.user) # MyApp.Accounts.create_user_profile(changeset.data.user_profile) socket = put_flash(socket, :info, "注册成功!") {:noreply, push_navigate(socket, to: ~p"/")} else {:noreply, assign(socket, form: to_form(changeset))} end end end
3. 组件抽离优化(可选)
如果觉得render函数里的步骤代码太臃肿,可以把每一步抽成独立组件:
defmodule MyAppWeb.RegistrationLive.Step1 do use MyAppWeb, :component attr :form, :any, required: true def render(assigns) do ~H""" <.form for={@form} phx-submit="next-step" class="step-form"> <div class="form-group"> <%= label @form, :email, "邮箱" %> <%= email_input @form, :email, class: "form-control" %> <%= error_tag @form, :email %> </div> <%= submit "下一步", class: "btn btn-primary" %> </.form> """ end end defmodule MyAppWeb.RegistrationLive.Step2 do use MyAppWeb, :component attr :form, :any, required: true def render(assigns) do ~H""" <.form for={@form} phx-submit="submit-registration" class="step-form"> <div class="form-group"> <%= label @form, :"user_profile.phone_number", "手机号" %> <%= text_input @form, :"user_profile.phone_number", class: "form-control" %> <%= error_tag @form, :"user_profile.phone_number" %> </div> <div class="form-group"> <%= label @form, :"user_profile.password", "密码" %> <%= password_input @form, :"user_profile.password", class: "form-control" %> <%= error_tag @form, :"user_profile.password" %> </div> <div class="form-group"> <%= label @form, :"user_profile.confirm_password", "确认密码" %> <%= password_input @form, :"user_profile.confirm_password", class: "form-control" %> <%= error_tag @form, :"user_profile.confirm_password" %> </div> <button type="button" phx-click="prev-step" class="btn btn-secondary">上一步</button> <%= submit "完成注册", class: "btn btn-primary" %> </.form> """ end end
然后在LiveView的render里替换成组件调用:
<%= if @current_step == 1 do %> <.live_component module={MyAppWeb.RegistrationLive.Step1} id="step1" form={@form} /> <% elsif @current_step == 2 do %> <.live_component module={MyAppWeb.RegistrationLive.Step2} id="step2" form={@form} /> <% end %>
关键注意点
- 嵌入式Schema的表单字段要通过
:user.email、:user_profile.phone_number这种嵌套方式访问 - 分步验证时,每一步的变化集只验证当前步骤的字段,避免提前出现不必要的错误提示
- 步骤切换时要复用已保存的
registration数据,确保已输入的内容不会丢失
内容的提问来源于stack exchange,提问作者Zain
相关产品推荐
相关产品推荐

