Skip to content

表单事件

表单事件(Form Events)专门用于处理 HTML 表单相关的交互,包括表单提交、重置、字段值变化、焦点获取/失去以及输入验证等。掌握表单事件对于构建交互式表单、实时验证和用户体验优化至关重要。

前置知识

阅读本节前,建议先了解:键盘事件

基础概念

表单事件类型

事件触发时机绑定元素
submit表单提交时<form>
reset表单重置时<form>
change值改变且失去焦点时表单控件
input值每次变化时表单控件
focus获取焦点时元素
blur失去焦点时元素
focusin获取焦点时(冒泡)元素
focusout失去焦点时(冒泡)元素
invalid验证失败时表单控件
html
<!-- HTML 事件属性方式 -->
<form onsubmit="handleSubmit(event)">
  <input onfocus="this.style.borderColor='blue'"
         onblur="this.style.borderColor='gray'"
         oninput="validateField(this)">
  <button type="submit">提交</button>
</form>

input vs change 的区别

特性inputchange
触发时机每次值变化时值变化 + 失去焦点后
文本输入每次按键触发失去焦点时触发
复选框点击时触发点击时触发
下拉选择选择时触发选择时触发
适用场景实时反馈确认更改

语法

HTML 事件属性

html
<form onsubmit="return validateForm()" onreset="handleReset()">
  <input oninput="liveSearch(this.value)"
         onchange="recordChange()"
         onfocus="showHelp()"
         onblur="hideHelp()"
         oninvalid="showError(this)">
  <select onchange="updateCategory(this.value)">
    <option>选项 1</option>
  </select>
</form>

JavaScript 绑定

javascript
const form = document.getElementById('my-form');

// 表单提交
form.addEventListener('submit', (e) => {
  e.preventDefault();
  const formData = new FormData(form);
  console.log('表单数据:', Object.fromEntries(formData));
});

// 实时输入监听
form.querySelector('input').addEventListener('input', (e) => {
  console.log('当前值:', e.target.value);
});

详细说明

submit 事件

submit 事件在用户点击提交按钮或按 Enter 键提交表单时触发。这是处理表单数据的核心事件。

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>表单提交事件</title>
</head>
<body>
  <form id="login-form">
    <label>用户名:<input type="text" name="username" required minlength="3"></label>
    <label>密码:<input type="password" name="password" required minlength="6"></label>
    <button type="submit">登录</button>
  </form>

  <script>
    const form = document.getElementById('login-form');

    form.addEventListener('submit', (e) => {
      // 阻止默认提交行为
      e.preventDefault();

      // 获取表单数据
      const formData = new FormData(form);

      // 方式 1:转为对象
      const data = Object.fromEntries(formData.entries());
      console.log('用户名:', data.username);
      console.log('密码:', data.password);

      // 方式 2:逐个获取
      const username = form.username.value;
      const password = form.password.value;

      // 发送 AJAX 请求
      fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      }).then(response => {
        if (response.ok) {
          console.log('登录成功');
        }
      });
    });
  </script>
</body>
</html>

focus / blur / focusin / focusout

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>焦点事件示例</title>
  <style>
    .form-group {
      margin-bottom: 16px;
      position: relative;
    }

    .form-group label {
      display: block;
      margin-bottom: 4px;
      font-weight: 600;
    }

    .form-group input {
      width: 100%;
      padding: 8px 12px;
      border: 2px solid #e2e8f0;
      border-radius: 6px;
      transition: border-color 0.2s;
    }

    .form-group input:focus {
      border-color: #3b82f6;
      outline: none;
      box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
    }

    .form-group .help-text {
      font-size: 13px;
      color: #64748b;
      margin-top: 4px;
      display: none;
    }

    .form-group .error-text {
      font-size: 13px;
      color: #dc2626;
      margin-top: 4px;
      display: none;
    }

    .form-group.focused .help-text { display: block; }
    .form-group.has-error .error-text { display: block; }
  </style>
</head>
<body>
  <form id="contact-form">
    <div class="form-group">
      <label for="email">邮箱</label>
      <input type="email" id="email" name="email" required>
      <p class="help-text">请输入有效的邮箱地址</p>
      <p class="error-text">邮箱格式不正确</p>
    </div>

    <div class="form-group">
      <label for="phone">手机号</label>
      <input type="tel" id="phone" name="phone">
      <p class="help-text">11 位手机号码</p>
      <p class="error-text">请输入有效的手机号</p>
    </div>

    <button type="submit">提交</button>
  </form>

  <script>
    const form = document.getElementById('contact-form');

    // 使用 focusin/focusout(冒泡)在 form 上统一监听
    form.addEventListener('focusin', (e) => {
      e.target.closest('.form-group')?.classList.add('focused');
    });

    form.addEventListener('focusout', (e) => {
      const group = e.target.closest('.form-group');
      group?.classList.remove('focused');
      // 失焦时验证
      validateField(e.target);
    });

    function validateField(input) {
      const group = input.closest('.form-group');
      if (!group) return;

      if (!input.checkValidity()) {
        group.classList.add('has-error');
      } else {
        group.classList.remove('has-error');
      }
    }
  </script>
</body>
</html>

invalid 事件

javascript
// 监听表单控件的验证失败事件
document.querySelectorAll('input, select, textarea').forEach(field => {
  field.addEventListener('invalid', (e) => {
    console.log('验证失败:', e.target.name, e.target.validationMessage);
  });
});

// 监听表单级别的 invalid 事件(阻止默认提示)
form.addEventListener('invalid', (e) => {
  e.preventDefault(); // 阻止浏览器默认的气泡提示
  // 显示自定义错误消息
}, true); // 使用捕获阶段

实战示例

完整的实时验证表单

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>实时验证表单</title>
  <style>
    .form-field {
      margin-bottom: 20px;
    }
    .form-field label {
      display: block;
      font-weight: 600;
      margin-bottom: 4px;
    }
    .form-field input {
      width: 100%;
      padding: 10px;
      border: 2px solid #e2e8f0;
      border-radius: 6px;
      box-sizing: border-box;
    }
    .form-field input:valid { border-color: #22c55e; }
    .form-field input:invalid:not(:placeholder-shown) { border-color: #ef4444; }
    .form-field .message {
      font-size: 13px;
      margin-top: 4px;
      min-height: 20px;
    }
    .form-field .message.error { color: #ef4444; }
    .form-field .message.success { color: #22c55e; }

    .password-strength {
      height: 4px;
      background: #e2e8f0;
      border-radius: 2px;
      margin-top: 8px;
      overflow: hidden;
    }
    .password-strength .bar {
      height: 100%;
      border-radius: 2px;
      transition: width 0.3s, background 0.3s;
      width: 0;
    }
  </style>
</head>
<body>
  <form id="register-form" novalidate>
    <div class="form-field">
      <label for="username">用户名</label>
      <input type="text" id="username" minlength="3" maxlength="20"
             placeholder="3-20 个字符" required>
      <div class="message" id="username-msg"></div>
    </div>

    <div class="form-field">
      <label for="password">密码</label>
      <input type="password" id="password" minlength="8"
             placeholder="至少 8 个字符" required>
      <div class="password-strength">
        <div class="bar" id="strength-bar"></div>
      </div>
      <div class="message" id="password-msg"></div>
    </div>

    <div class="form-field">
      <label for="confirm">确认密码</label>
      <input type="password" id="confirm" placeholder="再次输入密码" required>
      <div class="message" id="confirm-msg"></div>
    </div>

    <button type="submit">注册</button>
  </form>

  <script>
    const form = document.getElementById('register-form');
    const username = document.getElementById('username');
    const password = document.getElementById('password');
    const confirm = document.getElementById('confirm');
    const strengthBar = document.getElementById('strength-bar');

    // 实时输入验证
    username.addEventListener('input', () => {
      const msg = document.getElementById('username-msg');
      const val = username.value;

      if (val.length === 0) {
        msg.textContent = '';
        msg.className = 'message';
      } else if (val.length < 3) {
        msg.textContent = '用户名至少 3 个字符';
        msg.className = 'message error';
      } else if (!/^[a-zA-Z0-9_]+$/.test(val)) {
        msg.textContent = '只允许字母、数字和下划线';
        msg.className = 'message error';
      } else {
        msg.textContent = '用户名可用';
        msg.className = 'message success';
      }
    });

    // 密码强度检测
    password.addEventListener('input', () => {
      const val = password.value;
      const msg = document.getElementById('password-msg');
      let strength = 0;

      if (val.length >= 8) strength++;
      if (/[a-z]/.test(val) && /[A-Z]/.test(val)) strength++;
      if (/\d/.test(val)) strength++;
      if (/[^a-zA-Z0-9]/.test(val)) strength++;

      const colors = ['#ef4444', '#f59e0b', '#22c55e', '#16a34a'];
      const labels = ['弱', '一般', '强', '非常强'];

      if (val.length > 0) {
        strengthBar.style.width = `${strength * 25}%`;
        strengthBar.style.background = colors[strength - 1] || '#e2e8f0';
        msg.textContent = `密码强度:${labels[strength - 1] || '太弱'}`;
        msg.className = 'message ' + (strength >= 2 ? 'success' : 'error');
      } else {
        strengthBar.style.width = '0';
        msg.textContent = '';
        msg.className = 'message';
      }
    });

    // 确认密码匹配
    confirm.addEventListener('input', () => {
      const msg = document.getElementById('confirm-msg');
      if (confirm.value === password.value && confirm.value.length > 0) {
        msg.textContent = '密码匹配';
        msg.className = 'message success';
      } else if (confirm.value.length > 0) {
        msg.textContent = '密码不匹配';
        msg.className = 'message error';
      } else {
        msg.textContent = '';
        msg.className = 'message';
      }
    });

    // 提交验证
    form.addEventListener('submit', (e) => {
      if (!form.checkValidity()) {
        e.preventDefault();
        // 显示第一个无效字段的错误
        const firstInvalid = form.querySelector(':invalid');
        if (firstInvalid) firstInvalid.focus();
      } else {
        e.preventDefault();
        console.log('表单验证通过,可以提交');
      }
    });
  </script>
</body>
</html>

注意事项

submit 事件的触发方式

submit 事件在以下情况触发:

  • 点击 <button type="submit"><input type="submit">
  • 在文本输入框中按 Enter 键
  • 通过 JavaScript 调用 form.submit()不会触发 submit 事件)
javascript
// 会触发 submit 事件
form.querySelector('button[type="submit"]').click();

// 不会触发 submit 事件
form.submit();

// 如需在 form.submit() 前执行验证,应手动处理
function submitForm() {
  if (form.checkValidity()) {
    form.submit();
  }
}

change 事件在不同控件上的行为

控件change 触发时机
<input type="text">失去焦点且值已改变
<input type="checkbox">点击切换时
<input type="radio">选中时
<input type="range">释放鼠标时
<select>选择选项时
<textarea>失去焦点且值已改变

最佳实践

1. 使用 input 事件进行实时反馈

javascript
// 推荐:input 事件实时响应
input.addEventListener('input', () => {
  updatePreview(input.value);
});

// 不推荐:change 事件延迟反馈
input.addEventListener('change', () => {
  updatePreview(input.value);
});

2. 使用 FormData 收集表单数据

javascript
form.addEventListener('submit', (e) => {
  e.preventDefault();
  const formData = new FormData(form);

  // 转为 JSON 对象
  const data = Object.fromEntries(formData);
  console.log(data);

  // 或遍历
  for (const [key, value] of formData.entries()) {
    console.log(key, value);
  }
});

下一节

继续学习:窗口事件

参考链接