Back

使用原生 JavaScript 处理表单输入:无需框架

使用原生 JavaScript 处理表单输入:无需框架

表单是网页用户交互的基础。虽然 React 和 Vue 等框架提供了便捷的抽象,但了解如何使用原生 JavaScript 处理表单能让您完全掌控并消除不必要的依赖。

本指南将带您了解 JavaScript 原生表单处理的所有知识——从选择元素和捕获值到验证输入和处理提交。

关键要点

  • 原生 JavaScript 提供了无需框架即可完成表单处理的所有工具
  • 使用 event.preventDefault() 控制表单提交行为
  • 利用 HTML5 约束验证以最少的代码实现基本验证
  • 通过 inputblur 事件实现实时验证,提升用户体验
  • FormData API 简化了表单值的收集和处理
  • 实现自定义表单验证时始终考虑可访问性

JavaScript 基础表单处理

让我们从一个简单的 HTML 表单开始:

<form id="signup-form">
  <div>
    <label for="username">Username:</label>
    <input type="text" id="username" name="username">
  </div>
  <div>
    <label for="email">Email:</label>
    <input type="email" id="email" name="email">
  </div>
  <div>
    <label for="password">Password:</label>
    <input type="password" id="password" name="password">
  </div>
  <button type="submit">Sign Up</button>
</form>

选择表单元素

您可以通过多种方式访问表单及其元素:

// 通过 ID 选择(推荐用于特定表单)
const form = document.getElementById('signup-form');

// 使用 querySelector 选择
const form = document.querySelector('#signup-form');

// 访问表单元素集合
const forms = document.forms;
const signupForm = forms['signup-form']; // 通过 id/name
const firstForm = forms[0]; // 通过索引

捕获提交事件

要处理表单提交,请为表单添加事件监听器:

const form = document.getElementById('signup-form');

form.addEventListener('submit', function(event) {
  // 阻止默认的表单提交行为
  event.preventDefault();
  
  // 在此处理表单数据
  console.log('Form submitted!');
});

preventDefault() 方法阻止浏览器提交表单和刷新页面,让您控制接下来发生的事情。

访问表单输入值

有多种方式访问表单输入值:

方法 1:访问单个元素

form.addEventListener('submit', function(event) {
  event.preventDefault();
  
  // 从单个表单元素获取值
  const username = document.getElementById('username').value;
  const email = document.getElementById('email').value;
  const password = document.getElementById('password').value;
  
  console.log(username, email, password);
});

方法 2:使用 form.elements

elements 属性提供对所有表单控件的访问:

form.addEventListener('submit', function(event) {
  event.preventDefault();
  
  // 通过 elements 集合访问
  const username = form.elements.username.value;
  const email = form.elements.email.value;
  const password = form.elements.password.value;
  
  console.log(username, email, password);
});

方法 3:使用 FormData API

FormData API 提供了一种简洁的方式来捕获所有表单值:

form.addEventListener('submit', function(event) {
  event.preventDefault();
  
  // 从表单创建 FormData 对象
  const formData = new FormData(form);
  
  // 访问单个值
  const username = formData.get('username');
  const email = formData.get('email');
  
  // 转换为普通对象
  const formObject = Object.fromEntries(formData);
  console.log(formObject); // {username: '...', email: '...', password: '...'}
});

使用 HTML5 约束进行表单验证

现代 HTML5 提供了无需 JavaScript 即可工作的内置验证属性:

<form id="signup-form">
  <div>
    <label for="username">Username:</label>
    <input type="text" id="username" name="username" required minlength="3">
  </div>
  <div>
    <label for="email">Email:</label>
    <input type="email" id="email" name="email" required>
  </div>
  <div>
    <label for="password">Password:</label>
    <input type="password" id="password" name="password" required minlength="8" 
           pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).*$">
    <small>Password must contain at least 8 characters, including uppercase, lowercase, and numbers</small>
  </div>
  <button type="submit">Sign Up</button>
</form>

常见的验证属性包括:

  • required:字段必须填写
  • minlength/maxlength:文本的最小/最大长度
  • min/max:数字输入的最小/最大值
  • pattern:要匹配的正则表达式模式
  • type="email":验证电子邮件格式
  • type="url":验证 URL 格式

利用约束验证 API

JavaScript 可以访问浏览器的内置验证系统:

form.addEventListener('submit', function(event) {
  // 使用约束验证 API 检查表单是否有效
  if (!form.checkValidity()) {
    // 表单无效 - 让浏览器处理错误显示
    return;
  }
  
  // 如果执行到这里,表单是有效的
  event.preventDefault();
  
  // 处理有效的表单数据
  console.log('Form is valid, processing data...');
});

您也可以检查特定输入的有效性:

const emailInput = document.getElementById('email');

// 检查电子邮件是否有效
if (emailInput.validity.valid) {
  console.log('Email is valid');
} else {
  // 检查特定的验证失败
  if (emailInput.validity.typeMismatch) {
    console.log('Email format is incorrect');
  }
  if (emailInput.validity.valueMissing) {
    console.log('Email is required');
  }
}

自定义验证和错误消息

虽然 HTML5 验证很方便,但您经常需要自定义验证逻辑和错误消息:

form.addEventListener('submit', function(event) {
  event.preventDefault();
  
  const username = document.getElementById('username');
  const email = document.getElementById('email');
  const password = document.getElementById('password');
  
  // 清除之前的错误消息
  clearErrors();
  
  let isValid = true;
  
  // 自定义用户名验证
  if (username.value.trim() === '') {
    displayError(username, 'Username is required');
    isValid = false;
  } else if (username.value.length < 3) {
    displayError(username, 'Username must be at least 3 characters');
    isValid = false;
  }
  
  // 自定义电子邮件验证
  if (!isValidEmail(email.value)) {
    displayError(email, 'Please enter a valid email address');
    isValid = false;
  }
  
  // 自定义密码验证
  if (password.value.length < 8) {
    displayError(password, 'Password must be at least 8 characters');
    isValid = false;
  } else if (!/[A-Z]/.test(password.value)) {
    displayError(password, 'Password must contain at least one uppercase letter');
    isValid = false;
  }
  
  // 如果有效,处理表单
  if (isValid) {
    console.log('Form is valid, submitting...');
    // 提交表单数据
  }
});

function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function displayError(input, message) {
  const formControl = input.parentElement;
  const errorElement = document.createElement('div');
  errorElement.className = 'error-message';
  errorElement.textContent = message;
  formControl.appendChild(errorElement);
  input.classList.add('error-input');
}

function clearErrors() {
  document.querySelectorAll('.error-message').forEach(error => error.remove());
  document.querySelectorAll('.error-input').forEach(input => {
    input.classList.remove('error-input');
  });
}

使用输入事件进行实时验证

为了更好的用户体验,在用户输入时进行验证:

const emailInput = document.getElementById('email');

emailInput.addEventListener('input', function() {
  if (this.value && !isValidEmail(this.value)) {
    this.setCustomValidity('Please enter a valid email address');
  } else {
    this.setCustomValidity('');
  }
});

function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

input 事件在用户更改值时触发,而 change 事件在输入失去焦点且值已更改后触发。

处理不同的输入类型

单选按钮

<div>
  <p>Subscription plan:</p>
  <label>
    <input type="radio" name="plan" value="free" checked> Free
  </label>
  <label>
    <input type="radio" name="plan" value="premium"> Premium
  </label>
</div>
// 获取选中的单选按钮值
const selectedPlan = document.querySelector('input[name="plan"]:checked').value;

// 监听变化
document.querySelectorAll('input[name="plan"]').forEach(radio => {
  radio.addEventListener('change', function() {
    console.log('Selected plan:', this.value);
  });
});

复选框

<div>
  <p>Interests:</p>
  <label>
    <input type="checkbox" name="interests" value="javascript"> JavaScript
  </label>
  <label>
    <input type="checkbox" name="interests" value="html"> HTML
  </label>
  <label>
    <input type="checkbox" name="interests" value="css"> CSS
  </label>
</div>
// 获取所有选中的值
function getSelectedInterests() {
  const checkboxes = document.querySelectorAll('input[name="interests"]:checked');
  const values = Array.from(checkboxes).map(cb => cb.value);
  return values;
}

// 在表单提交时
form.addEventListener('submit', function(event) {
  event.preventDefault();
  const interests = getSelectedInterests();
  console.log('Selected interests:', interests);
});

下拉选择框

<div>
  <label for="country">Country:</label>
  <select id="country" name="country">
    <option value="">Select a country</option>
    <option value="us">United States</option>
    <option value="ca">Canada</option>
    <option value="uk">United Kingdom</option>
  </select>
</div>
// 获取选中的值
const country = document.getElementById('country').value;

// 监听变化
document.getElementById('country').addEventListener('change', function() {
  console.log('Selected country:', this.value);
});

完整的表单处理示例

让我们将所有内容整合到一个完整的示例中:

document.addEventListener('DOMContentLoaded', function() {
  const form = document.getElementById('signup-form');
  
  // 添加实时验证
  setupRealTimeValidation();
  
  form.addEventListener('submit', function(event) {
    event.preventDefault();
    
    if (validateForm()) {
      // 创建包含表单数据的对象
      const formData = new FormData(form);
      const userData = Object.fromEntries(formData);
      
      // 这里通常会将数据发送到服务器
      console.log('Submitting user data:', userData);
      
      // 模拟 API 调用
      submitFormData(userData)
        .then(response => {
          showSuccessMessage('Account created successfully!');
          form.reset();
        })
        .catch(error => {
          showErrorMessage('Failed to create account. Please try again.');
        });
    }
  });
  
  function validateForm() {
    // 清除之前的错误
    clearErrors();
    
    let isValid = true;
    const username = form.elements.username;
    const email = form.elements.email;
    const password = form.elements.password;
    
    // 验证用户名
    if (username.value.trim() === '') {
      displayError(username, 'Username is required');
      isValid = false;
    } else if (username.value.length < 3) {
      displayError(username, 'Username must be at least 3 characters');
      isValid = false;
    }
    
    // 验证电子邮件
    if (email.value.trim() === '') {
      displayError(email, 'Email is required');
      isValid = false;
    } else if (!isValidEmail(email.value)) {
      displayError(email, 'Please enter a valid email address');
      isValid = false;
    }
    
    // 验证密码
    if (password.value === '') {
      displayError(password, 'Password is required');
      isValid = false;
    } else if (password.value.length < 8) {
      displayError(password, 'Password must be at least 8 characters');
      isValid = false;
    } else if (!/[A-Z]/.test(password.value) || !/[a-z]/.test(password.value) || !/[0-9]/.test(password.value)) {
      displayError(password, 'Password must include uppercase, lowercase, and numbers');
      isValid = false;
    }
    
    return isValid;
  }
  
  function setupRealTimeValidation() {
    const inputs = form.querySelectorAll('input');
    
    inputs.forEach(input => {
      input.addEventListener('input', function() {
        // 用户开始输入时清除错误
        const errorElement = this.parentElement.querySelector('.error-message');
        if (errorElement) {
          errorElement.remove();
          this.classList.remove('error-input');
        }
      });
    });
    
    // 电子邮件特定验证
    form.elements.email.addEventListener('blur', function() {
      if (this.value && !isValidEmail(this.value)) {
        displayError(this, 'Please enter a valid email address');
      }
    });
  }
  
  // 辅助函数
  function isValidEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
  
  function displayError(input, message) {
    const formControl = input.parentElement;
    const errorElement = document.createElement('div');
    errorElement.className = 'error-message';
    errorElement.textContent = message;
    formControl.appendChild(errorElement);
    input.classList.add('error-input');
  }
  
  function clearErrors() {
    document.querySelectorAll('.error-message').forEach(error => error.remove());
    document.querySelectorAll('.error-input').forEach(input => {
      input.classList.remove('error-input');
    });
  }
  
  function showSuccessMessage(message) {
    const messageElement = document.createElement('div');
    messageElement.className = 'success-message';
    messageElement.textContent = message;
    form.parentElement.insertBefore(messageElement, form);
    
    // 3秒后移除
    setTimeout(() => {
      messageElement.remove();
    }, 3000);
  }
  
  function showErrorMessage(message) {
    const messageElement = document.createElement('div');
    messageElement.className = 'error-banner';
    messageElement.textContent = message;
    form.parentElement.insertBefore(messageElement, form);
    
    // 3秒后移除
    setTimeout(() => {
      messageElement.remove();
    }, 3000);
  }
  
  // 模拟 API 提交
  function submitFormData(data) {
    return new Promise((resolve, reject) => {
      // 模拟网络请求
      setTimeout(() => {
        // 演示用90%成功率
        if (Math.random() > 0.1) {
          resolve({ success: true, message: 'User created' });
        } else {
          reject({ success: false, message: 'Server error' });
        }
      }, 1500);
    });
  }
});

表单可访问性考虑

使用 JavaScript 处理表单时,确保它们保持可访问性:

  1. 维护焦点管理:显示错误时,将焦点设置到第一个无效字段
  2. 使用 ARIA 属性:为无效字段添加 aria-invalid="true"
  3. 关联错误消息:使用 aria-describedby 将输入与其错误消息连接
  4. 提供清晰的说明:使用 <label> 元素和描述性错误消息
  5. 支持键盘导航:确保所有表单控件都可以通过键盘访问

可访问错误处理的示例:

function displayError(input, message) {
  const formControl = input.parentElement;
  const errorId = `${input.id}-error`;
  
  const errorElement = document.createElement('div');
  errorElement.id = errorId;
  errorElement.className = 'error-message';
  errorElement.textContent = message;
  
  formControl.appendChild(errorElement);
  
  // 添加可访问性属性
  input.setAttribute('aria-invalid', 'true');
  input.setAttribute('aria-describedby', errorId);
  
  // 将焦点设置到第一个无效输入
  if (!document.querySelector('.error-input')) {
    input.focus();
  }
  
  input.classList.add('error-input');
}

常见问题

在表单元素上使用内置的 reset 方法:document.getElementById('myForm').reset();

使用约束验证 API 的 checkValidity 方法:const isValid = form.checkValidity();

要异步提交表单数据,请将 Fetch API 与 FormData 对象结合使用。首先,从表单创建 FormData 实例。然后使用 fetch 以 POST 方法发送。确保处理响应并捕获提交过程中的任何错误。

要处理文件上传,请访问文件输入元素的 files 属性,获取选中的文件,并在使用 fetch 或其他方法提交之前将其附加到 FormData 对象。

`input` 事件在输入值发生变化时触发,而 `change` 事件仅在输入失去焦点且值自获得焦点以来已更改时触发。

要动态添加表单字段,请使用 document.createElement 创建新的输入元素,设置其类型和名称,然后将其附加到表单中的容器元素。

结论

通过掌握原生 JavaScript 表单处理,您可以在不依赖外部库的情况下完全控制表单。这种方法减少了依赖,提高了性能,并加深了您对网页表单实际工作原理的理解。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers