表单处理与验证
表单处理与验证
表单是 Web 应用中最常见的交互元素。htmx 让表单处理变得异常简洁:无需手动阻止默认提交、无需手动序列化数据、无需处理响应后的 DOM 更新。本文将深入讲解 htmx 表单处理的完整模式,包括验证、错误回显和文件上传。
htmx 表单基础
在 htmx 中,表单与普通 HTML 表单几乎完全相同,只需添加 hx-post 属性即可启用 AJAX 提交。
<form hx-post="/api/contact" hx-target="#result" hx-swap="innerHTML">
<input type="text" name="name" placeholder="姓名" required />
<input type="email" name="email" placeholder="邮箱" required />
<textarea name="message" placeholder="留言内容" required></textarea>
<button type="submit">提交</button>
</form>
<div id="result"></div>表单提交时,htmx 会自动:
- 阻止表单的默认提交行为
- 序列化所有表单字段(包括选中的 checkbox、radio)
- 以
application/x-www-form-urlencoded编码发送 POST 请求 - 将服务器返回的 HTML 替换到
#result
提示:htmx 会尊重 HTML5 的
required、pattern、min、max等原生验证属性。不通过原生验证的表单不会触发 htmx 请求。
客户端验证
HTML5 原生验证是客户端验证的第一道防线:
<form hx-post="/api/register" hx-target="#feedback">
<input type="email"
name="email"
placeholder="邮箱"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
title="请输入有效的邮箱地址" />
<input type="password"
name="password"
placeholder="密码"
required
minlength="8"
title="密码至少 8 位" />
<button type="submit">注册</button>
</form>htmx 在触发请求前会先调用 reportValidity(),因此原生验证未通过的字段会阻止请求发送,并显示浏览器的默认验证提示。
自定义验证样式
利用 CSS 伪类美化验证状态:
input:invalid {
border-color: #D02020;
background-color: #FFF0F0;
}
input:valid {
border-color: #20A050;
}
input:focus:invalid {
outline-color: #D02020;
}服务器端验证与错误回显
客户端验证可以被绕过,服务器端验证是不可或缺的安全防线。htmx 的错误回显模式非常直观:服务器直接返回带有错误信息的 HTML 片段。
基本错误回显
<form hx-post="/api/login" hx-target="this" hx-swap="outerHTML">
<input type="email" name="email" placeholder="邮箱" required />
<input type="password" name="password" placeholder="密码" required />
<button type="submit">登录</button>
</form>服务端验证失败时返回替换后的完整表单:
@app.route('/api/login', methods=['POST'])
def login():
email = request.form.get('email')
password = request.form.get('password')
errors = {}
if not email:
errors['email'] = '邮箱不能为空'
if not password:
errors['password'] = '密码不能为空'
if not validate_credentials(email, password):
errors['general'] = '邮箱或密码错误'
if errors:
return render_template_string('''
<form hx-post="/api/login" hx-target="this" hx-swap="outerHTML">
<div class="error">{{ errors.get('general', '') }}</div>
<input type="email" name="email" value="{{ email }}"
class="{% if 'email' in errors %}invalid{% endif %}" />
<span class="error-msg">{{ errors.get('email', '') }}</span>
<input type="password" name="password"
class="{% if 'password' in errors %}invalid{% endif %}" />
<span class="error-msg">{{ errors.get('password', '') }}</span>
<button type="submit">登录</button>
</form>
''', email=email, errors=errors), 422
return '<div class="success">登录成功!</div>'这种方式的优点:
- 保留用户已输入的内容
- 错误信息与对应的输入字段紧邻
- 无需 JavaScript 解析 JSON 错误对象
字段级实时验证
对于需要即时反馈的场景(如用户名查重),可以对单个字段使用 htmx:
<form hx-post="/api/register" hx-target="#register-result">
<input type="text"
name="username"
placeholder="用户名"
hx-get="/api/check-username"
hx-trigger="blur changed"
hx-target="next .username-feedback"
hx-swap="innerHTML" />
<span class="username-feedback"></span>
<input type="email" name="email" placeholder="邮箱" required />
<button type="submit">注册</button>
</form>
<div id="register-result"></div>用户离开用户名输入框时,自动发送 GET 请求验证用户名可用性,结果实时显示在紧邻的 <span> 中。
文件上传
文件上传是表单处理中的特殊场景。htmx 通过 hx-encoding 属性支持 multipart/form-data 编码。
基础文件上传
<form hx-post="/api/upload"
hx-encoding="multipart/form-data"
hx-target="#upload-status"
hx-indicator="#upload-spinner">
<input type="file" name="avatar" accept="image/*" required />
<button type="submit">上传头像</button>
</form>
<span id="upload-spinner" class="htmx-indicator">上传中...</span>
<div id="upload-status"></div>注意:文件上传时必须设置
hx-encoding="multipart/form-data",否则 htmx 会以普通表单编码发送,导致文件数据丢失。
上传进度条
htmx 在文件上传期间触发 htmx:xhr:progress 事件,可以用来实现进度条:
<form hx-post="/api/upload-large"
hx-encoding="multipart/form-data"
hx-target="#upload-result">
<input type="file" name="document" />
<progress id="progress" value="0" max="100"></progress>
<button type="submit">上传</button>
</form>
<script>
document.addEventListener('htmx:xhr:progress', function(evt) {
const progress = evt.detail.loaded / evt.detail.total * 100;
document.getElementById('progress').value = progress;
});
</script>多文件上传
<form hx-post="/api/upload-multiple"
hx-encoding="multipart/form-data"
hx-target="#gallery"
hx-swap="beforeend">
<input type="file" name="images" multiple accept="image/*" />
<button type="submit">上传图片</button>
</form>
<div id="gallery"></div>multiple 属性允许选择多个文件,htmx 会自动将文件列表包含在 FormData 中发送。
多步骤表单(Wizard)
多步骤表单在 htmx 中实现非常自然:每一步提交后,服务器返回下一步的表单 HTML。
<!-- 步骤一:基本信息 -->
<form hx-post="/api/wizard/step1" hx-target="this" hx-swap="outerHTML">
<h3>步骤 1 / 3</h3>
<input type="text" name="fullname" placeholder="全名" required />
<input type="email" name="email" placeholder="邮箱" required />
<button type="submit">下一步</button>
</form>服务器端:
@app.route('/api/wizard/step1', methods=['POST'])
def wizard_step1():
# 验证并保存第一步数据到 session
session['fullname'] = request.form.get('fullname')
session['email'] = request.form.get('email')
# 返回第二步表单
return render_template_string('''
<form hx-post="/api/wizard/step2" hx-target="this" hx-swap="outerHTML">
<h3>步骤 2 / 3</h3>
<input type="text" name="company" placeholder="公司名称" required />
<input type="text" name="phone" placeholder="电话" required />
<button type="button" hx-get="/api/wizard/step1" hx-target="closest form" hx-swap="outerHTML">上一步</button>
<button type="submit">下一步</button>
</form>
''')提示:“上一步”按钮使用
hx-get从服务器重新获取上一步的表单,确保用户修改后能正确保存。不要仅在前端切换显示,否则刷新页面会丢失状态。
完整示例:注册表单
以下是一个完整的用户注册表单,涵盖客户端验证、服务器端验证、错误回显和成功提示:
<div id="register-container">
<form hx-post="/api/register"
hx-target="#register-container"
hx-swap="innerHTML"
hx-indicator="#register-spinner">
<div class="form-group">
<input type="text" name="username" placeholder="用户名" required
minlength="3" maxlength="20"
hx-get="/api/check-username"
hx-trigger="blur changed delay:300ms"
hx-target="next .field-error"
hx-swap="innerHTML" />
<span class="field-error"></span>
</div>
<div class="form-group">
<input type="email" name="email" placeholder="邮箱" required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$" />
</div>
<div class="form-group">
<input type="password" name="password" placeholder="密码" required
minlength="8" />
</div>
<div class="form-group">
<input type="password" name="password_confirm" placeholder="确认密码" required />
</div>
<button type="submit">
注册
<span id="register-spinner" class="htmx-indicator">...</span>
</button>
</form>
</div>服务端验证返回错误表单(保留输入值):
@app.route('/api/register', methods=['POST'])
def register():
data = request.form
errors = {}
if len(data.get('username', '')) < 3:
errors['username'] = '用户名至少 3 个字符'
if data.get('password') != data.get('password_confirm'):
errors['password_confirm'] = '两次输入的密码不一致'
if errors:
# 返回带错误的表单
return render_template('register_form.html', data=data, errors=errors), 422
# 创建用户
create_user(data)
return '<div class="success">注册成功!<a href="/login">去登录</a></div>'总结
htmx 的表单处理遵循”服务器返回 HTML”的核心理念,让表单验证和错误回显变得直观:
- 客户端验证:利用 HTML5 原生验证属性,htmx 自动阻止无效表单提交
- 服务器端验证:返回带有错误信息的完整表单 HTML,保留用户输入
- 字段级验证:对单个字段使用
hx-get实现实时异步验证 - 文件上传:设置
hx-encoding="multipart/form-data",监听htmx:xhr:progress实现进度条 - 多步骤表单:每一步提交后返回下一步的表单,天然支持前进/后退
这种模式的本质是把状态管理和 UI 更新都交给服务器,客户端只负责触发请求和插入响应。下一篇文章将学习 hx-swap-oob 与多元素更新,掌握一次请求更新页面多个区域的技术。
评论
Written by
AI-Writer