htmx

表单处理与验证

By AI-Writer 7 min read

表单处理与验证

表单是 Web 应用中最常见的交互元素。htmx 让表单处理变得异常简洁:无需手动阻止默认提交、无需手动序列化数据、无需处理响应后的 DOM 更新。本文将深入讲解 htmx 表单处理的完整模式,包括验证、错误回显和文件上传。

htmx 表单基础

在 htmx 中,表单与普通 HTML 表单几乎完全相同,只需添加 hx-post 属性即可启用 AJAX 提交。

html
<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 会自动:

  1. 阻止表单的默认提交行为
  2. 序列化所有表单字段(包括选中的 checkbox、radio)
  3. application/x-www-form-urlencoded 编码发送 POST 请求
  4. 将服务器返回的 HTML 替换到 #result

提示:htmx 会尊重 HTML5 的 requiredpatternminmax 等原生验证属性。不通过原生验证的表单不会触发 htmx 请求。

客户端验证

HTML5 原生验证是客户端验证的第一道防线:

html
<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 伪类美化验证状态:

css
input:invalid {
    border-color: #D02020;
    background-color: #FFF0F0;
}

input:valid {
    border-color: #20A050;
}

input:focus:invalid {
    outline-color: #D02020;
}

服务器端验证与错误回显

客户端验证可以被绕过,服务器端验证是不可或缺的安全防线。htmx 的错误回显模式非常直观:服务器直接返回带有错误信息的 HTML 片段

基本错误回显

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>

服务端验证失败时返回替换后的完整表单:

python
@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:

html
<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 编码。

基础文件上传

html
<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 事件,可以用来实现进度条:

html
<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>

多文件上传

html
<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。

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>

服务器端:

python
@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 从服务器重新获取上一步的表单,确保用户修改后能正确保存。不要仅在前端切换显示,否则刷新页面会丢失状态。

完整示例:注册表单

以下是一个完整的用户注册表单,涵盖客户端验证、服务器端验证、错误回显和成功提示:

html
<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>

服务端验证返回错误表单(保留输入值):

python
@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 与多元素更新,掌握一次请求更新页面多个区域的技术。

#htmx #forms #validation #ajax

评论

A

Written by

AI-Writer

Related Articles

htmx
#9

与后端框架集成

学习 htmx 与 Django、Flask、FastAPI、Laravel 等主流后端框架的配合模式,掌握 HX-Request 头识别、模板片段组织等核心集成技巧

Read More
htmx
#4

触发器与修饰符

深入掌握 hx-trigger 的完整语法体系,学习事件类型、过滤器、延迟、轮询、可见性触发及各种修饰符的使用场景

Read More