LiveView表单 #
一、表单基础 #
1.1 基本表单 #
elixir
defmodule HelloWeb.UserLive.Form do
use HelloWeb, :live_view
def mount(_params, _session, socket) do
changeset = Hello.Accounts.change_user(%User{})
{:ok, assign(socket, :changeset, changeset)}
end
def render(assigns) do
~H"""
<.simple_form :let={f} for={@changeset} phx-change="validate" phx-submit="save">
<.input field={f[:name]} type="text" label="Name" />
<.input field={f[:email]} type="email" label="Email" />
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
"""
end
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
%User{}
|> Hello.Accounts.change_user(user_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
def handle_event("save", %{"user" => user_params}, socket) do
case Hello.Accounts.create_user(user_params) do
{:ok, user} ->
{:noreply,
socket
|> put_flash(:info, "User created")
|> push_navigate(to: ~p"/users/#{user.id}")}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
end
1.2 表单组件 #
elixir
attr :for, :any, required: true
attr :as, :atom, default: nil
attr :phx_change, :string, default: nil
attr :phx_submit, :string, default: nil
def simple_form(assigns) do
~H"""
<.form
:let={f}
for={@for}
as={@as}
phx-change={@phx_change}
phx-submit={@phx_submit}
>
<%= render_slot(@inner_block, f) %>
</.form>
"""
end
1.3 输入组件 #
elixir
attr :field, Phoenix.HTML.FormField, required: true
attr :type, :atom, default: :text
attr :label, :string, default: nil
attr :placeholder, :string, default: nil
attr :required, :boolean, default: false
def input(assigns) do
~H"""
<div class="form-group">
<label for={@field.id}>
<%= @label || Phoenix.HTML.Form.humanize(@field.field) %>
</label>
<input
type={@type}
name={@field.name}
id={@field.id}
value={@field.value}
placeholder={@placeholder}
required={@required}
class={"form-control #{if @field.errors, do: "is-invalid"}"}
/>
<%= if @field.errors do %>
<div class="invalid-feedback">
<%= Enum.join(@field.errors, ", ") %>
</div>
<% end %>
</div>
"""
end
二、表单验证 #
2.1 实时验证 #
elixir
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
socket.assigns.user
|> Hello.Accounts.change_user(user_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
2.2 显示错误 #
elixir
attr :field, Phoenix.HTML.FormField, required: true
def error_tag(assigns) do
~H"""
<%= if @field.errors do %>
<div class="text-red-500 text-sm mt-1">
<%= for error <- @field.errors do %>
<p><%= translate_error(error) %></p>
<% end %>
</div>
<% end %>
"""
end
2.3 条件验证 #
elixir
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
socket.assigns.user
|> Hello.Accounts.change_user(user_params)
|> maybe_validate_password(user_params)
{:noreply, assign(socket, :changeset, changeset)}
end
defp maybe_validate_password(changeset, %{"password" => ""}) do
changeset
end
defp maybe_validate_password(changeset, %{"password" => password}) do
changeset
|> Ecto.Changeset.validate_length(:password, min: 8)
end
三、复杂表单 #
3.1 嵌套表单 #
elixir
def render(assigns) do
~H"""
<.simple_form :let={f} for={@changeset} phx-change="validate" phx-submit="save">
<.input field={f[:name]} type="text" label="Name" />
<h3>Address</h3>
<.inputs_for :let={af} field={f[:address]}>
<.input field={af[:street]} type="text" label="Street" />
<.input field={af[:city]} type="text" label="City" />
<.input field={af[:zip]} type="text" label="ZIP" />
</.inputs_for>
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
"""
end
3.2 动态表单字段 #
elixir
def render(assigns) do
~H"""
<.simple_form :let={f} for={@changeset} phx-change="validate" phx-submit="save">
<.input field={f[:title]} type="text" label="Title" />
<h3>Options</h3>
<.inputs_for :let={of} field={f[:options]}>
<div class="option-row">
<.input field={of[:key]} type="text" placeholder="Key" />
<.input field={of[:value]} type="text" placeholder="Value" />
<button type="button" phx-click="remove_option" phx-value-index={of.index}>
Remove
</button>
</div>
</.inputs_for>
<button type="button" phx-click="add_option">Add Option</button>
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
"""
end
def handle_event("add_option", _params, socket) do
options = socket.assigns.changeset.changes.options || []
new_options = options ++ [%{key: "", value: ""}]
changeset = Ecto.Changeset.put_change(
socket.assigns.changeset,
:options,
new_options
)
{:noreply, assign(socket, :changeset, changeset)}
end
def handle_event("remove_option", %{"index" => index}, socket) do
index = String.to_integer(index)
options = socket.assigns.changeset.changes.options || []
new_options = List.delete_at(options, index)
changeset = Ecto.Changeset.put_change(
socket.assigns.changeset,
:options,
new_options
)
{:noreply, assign(socket, :changeset, changeset)}
end
四、文件上传 #
4.1 配置上传 #
elixir
def mount(_params, _session, socket) do
socket =
socket
|> assign(:changeset, Hello.Accounts.change_user(%User{}))
|> allow_upload(:avatar,
accept: ~w(.jpg .jpeg .png),
max_entries: 1,
max_file_size: 5_000_000
)
{:ok, socket}
end
4.2 上传表单 #
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
4.3 处理上传 #
elixir
def handle_event("save", %{"user" => user_params}, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
dest = Path.join("priv/static/uploads", Path.basename(path))
File.cp!(path, dest)
Routes.static_path(socket, "/uploads/#{Path.basename(dest)}")
end)
user_params = Map.put(user_params, :avatar, List.first(uploaded_files))
case Hello.Accounts.create_user(user_params) do
{:ok, user} ->
{:noreply,
socket
|> put_flash(:info, "User created")
|> push_navigate(to: ~p"/users/#{user.id}")}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
4.4 多文件上传 #
elixir
def mount(_params, _session, socket) do
socket =
socket
|> assign(:changeset, Hello.Blog.change_post(%Post{}))
|> allow_upload(:images,
accept: ~w(.jpg .jpeg .png),
max_entries: 5,
max_file_size: 10_000_000
)
{:ok, socket}
end
4.5 拖拽上传 #
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
五、表单状态管理 #
5.1 保存表单状态 #
elixir
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
socket.assigns.user
|> Hello.Accounts.change_user(user_params)
|> Map.put(:action, :validate)
socket =
socket
|> assign(:changeset, changeset)
|> assign(:form_data, user_params)
{:noreply, socket}
end
5.2 重置表单 #
elixir
def handle_event("reset", _params, socket) do
changeset = Hello.Accounts.change_user(%User{})
{:noreply, assign(socket, :changeset, changeset)}
end
5.3 自动保存 #
elixir
def handle_event("autosave", %{"user" => user_params}, socket) do
case Hello.Accounts.update_user(socket.assigns.user, user_params) do
{:ok, user} ->
{:noreply,
socket
|> assign(:user, user)
|> assign(:changeset, Hello.Accounts.change_user(user))}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
六、搜索表单 #
6.1 实时搜索 #
elixir
def render(assigns) do
~H"""
<div>
<.form phx-change="search">
<.input
type="text"
name="query"
value={@query}
placeholder="Search users..."
phx-debounce="300"
/>
</.form>
<ul>
<%= for user <- @users do %>
<li><%= user.name %> - <%= user.email %></li>
<% end %>
</ul>
</div>
"""
end
def handle_event("search", %{"query" => query}, socket) do
users = Hello.Accounts.search_users(query)
{:noreply, assign(socket, :users, users)}
end
6.2 防抖处理 #
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
七、最佳实践 #
7.1 使用changeset #
elixir
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
socket.assigns.user
|> Hello.Accounts.change_user(user_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
7.2 错误处理 #
elixir
def handle_event("save", %{"user" => user_params}, socket) do
case Hello.Accounts.create_user(user_params) do
{:ok, user} ->
{:noreply,
socket
|> put_flash(:info, "User created successfully")
|> push_navigate(to: ~p"/users/#{user.id}")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply,
socket
|> put_flash(:error, "Failed to create user")
|> assign(:changeset, changeset)}
end
end
7.3 加载状态 #
elixir
def render(assigns) do
~H"""
<.simple_form :let={f} for={@changeset} phx-change="validate" phx-submit="save">
<.input field={f[:name]} type="text" label="Name" />
<:actions>
<.button disabled={@saving}>
<%= if @saving, do: "Saving...", else: "Save" %>
</.button>
</:actions>
</.simple_form>
"""
end
def handle_event("save", params, socket) do
socket = assign(socket, :saving, true)
case save_user(params) do
{:ok, user} ->
{:noreply, push_navigate(socket, to: ~p"/users/#{user.id}")}
{:error, changeset} ->
{:noreply, assign(socket, changeset: changeset, saving: false)}
end
end
八、总结 #
8.1 核心概念 #
| 概念 | 说明 |
|---|---|
| .form | 表单组件 |
| .input | 输入组件 |
| phx-change | 变化事件 |
| phx-submit | 提交事件 |
| allow_upload | 文件上传 |
8.2 常用绑定 #
| 绑定 | 说明 |
|---|---|
| phx-debounce | 防抖 |
| phx-target | 目标组件 |
| phx-disable-with | 提交时禁用 |
8.3 下一步 #
现在你已经了解了LiveView表单,接下来让我们学习 上下文模式,深入了解业务逻辑组织!
最后更新:2026-03-28