表单事件
表单事件(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 的区别
| 特性 | input | change |
|---|---|---|
| 触发时机 | 每次值变化时 | 值变化 + 失去焦点后 |
| 文本输入 | 每次按键触发 | 失去焦点时触发 |
| 复选框 | 点击时触发 | 点击时触发 |
| 下拉选择 | 选择时触发 | 选择时触发 |
| 适用场景 | 实时反馈 | 确认更改 |
语法
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);
}
});下一节
继续学习:窗口事件