高级表单特性与交互技巧
表单验证
1. 内置验证属性
html
<form id="registration">
<!-- 必填字段 -->
<input type="text" required />
<!-- 模式匹配 -->
<input type="text" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}" title="请输入格式为XXX-XXX-XXXX的电话号码" />
<!-- 长度限制 -->
<input type="text" minlength="6" maxlength="12" />
<!-- 数值范围 -->
<input type="number" min="0" max="100" step="5" />
<!-- 自定义错误消息 -->
<input type="email" oninvalid="this.setCustomValidity('请输入有效的邮箱地址')" oninput="this.setCustomValidity('')" />
</form>
2. Constraint Validation API
javascript
const form = document.getElementById('registration')
const email = form.querySelector('input[type="email"]')
// 检查表单字段的有效性
function validateField(field) {
if (field.validity.valueMissing) {
field.setCustomValidity('此字段不能为空')
} else if (field.validity.typeMismatch) {
field.setCustomValidity('请输入正确的格式')
} else if (field.validity.tooShort) {
field.setCustomValidity(`最少需要${field.minLength}个字符`)
} else {
field.setCustomValidity('')
}
}
// 表单提交验证
form.addEventListener('submit', (event) => {
if (!form.checkValidity()) {
event.preventDefault()
Array.from(form.elements).forEach(validateField)
}
})
// 实时验证
email.addEventListener('input', () => {
validateField(email)
})
自定义表单控件
1. 自定义下拉选择器
html
<div class="custom-select" role="combobox" aria-expanded="false" aria-haspopup="listbox">
<button class="select-button" aria-label="选择选项">
<span class="selected-value">请选择</span>
<span class="arrow"></span>
</button>
<ul class="options-list" role="listbox">
<li role="option" data-value="1">选项 1</li>
<li role="option" data-value="2">选项 2</li>
<li role="option" data-value="3">选项 3</li>
</ul>
</div>
<style>
.custom-select {
position: relative;
width: 200px;
}
.select-button {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.options-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ccc;
border-top: none;
max-height: 200px;
overflow-y: auto;
display: none;
margin: 0;
padding: 0;
list-style: none;
}
.custom-select[aria-expanded='true'] .options-list {
display: block;
}
.options-list li {
padding: 8px;
cursor: pointer;
}
.options-list li:hover {
background: #f5f5f5;
}
</style>
<script>
class CustomSelect {
constructor(element) {
this.element = element
this.button = element.querySelector('.select-button')
this.optionsList = element.querySelector('.options-list')
this.selectedValue = element.querySelector('.selected-value')
this.setupEventListeners()
}
setupEventListeners() {
this.button.addEventListener('click', () => this.toggleDropdown())
this.optionsList.addEventListener('click', (e) => {
if (e.target.hasAttribute('data-value')) {
this.selectOption(e.target)
}
})
// 键盘导航
this.element.addEventListener('keydown', (e) => {
switch (e.key) {
case 'Enter':
this.toggleDropdown()
break
case 'Escape':
this.closeDropdown()
break
case 'ArrowDown':
e.preventDefault()
this.navigateOptions(1)
break
case 'ArrowUp':
e.preventDefault()
this.navigateOptions(-1)
break
}
})
}
toggleDropdown() {
const isExpanded = this.element.getAttribute('aria-expanded') === 'true'
this.element.setAttribute('aria-expanded', !isExpanded)
}
closeDropdown() {
this.element.setAttribute('aria-expanded', 'false')
}
selectOption(option) {
this.selectedValue.textContent = option.textContent
this.element.dispatchEvent(
new CustomEvent('change', {
detail: { value: option.dataset.value }
})
)
this.closeDropdown()
}
navigateOptions(direction) {
const options = Array.from(this.optionsList.children)
const currentIndex = options.findIndex((option) => option.classList.contains('focused'))
let nextIndex = currentIndex + direction
if (nextIndex < 0) nextIndex = options.length - 1
if (nextIndex >= options.length) nextIndex = 0
options.forEach((option) => option.classList.remove('focused'))
options[nextIndex].classList.add('focused')
options[nextIndex].scrollIntoView({ block: 'nearest' })
}
}
// 初始化所有自定义选择器
document.querySelectorAll('.custom-select').forEach((select) => {
new CustomSelect(select)
})
</script>
2. 自定义文件上传
html
<div class="custom-file-upload">
<input type="file" id="file-input" multiple accept="image/*" class="hidden" />
<label for="file-input" class="upload-trigger">
<span class="icon">📁</span>
<span class="text">点击或拖拽文件上传</span>
</label>
<div class="preview-container"></div>
</div>
<style>
.custom-file-upload {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
transition: border-color 0.3s;
}
.custom-file-upload.dragover {
border-color: #42b983;
}
.hidden {
display: none;
}
.upload-trigger {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
}
.preview-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 10px;
margin-top: 20px;
}
.preview-item {
position: relative;
padding-top: 100%;
}
.preview-item img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
</style>
<script>
class CustomFileUpload {
constructor(element) {
this.element = element
this.input = element.querySelector('input[type="file"]')
this.previewContainer = element.querySelector('.preview-container')
this.setupEventListeners()
}
setupEventListeners() {
// 文件选择
this.input.addEventListener('change', () => {
this.handleFiles(this.input.files)
})
// 拖拽上传
this.element.addEventListener('dragover', (e) => {
e.preventDefault()
this.element.classList.add('dragover')
})
this.element.addEventListener('dragleave', () => {
this.element.classList.remove('dragover')
})
this.element.addEventListener('drop', (e) => {
e.preventDefault()
this.element.classList.remove('dragover')
this.handleFiles(e.dataTransfer.files)
})
}
handleFiles(files) {
Array.from(files).forEach((file) => {
if (!file.type.startsWith('image/')) return
const reader = new FileReader()
reader.onload = (e) => this.createPreview(e.target.result)
reader.readAsDataURL(file)
})
}
createPreview(url) {
const preview = document.createElement('div')
preview.className = 'preview-item'
preview.innerHTML = `<img src="${url}" alt="预览图">`
this.previewContainer.appendChild(preview)
}
}
// 初始化所有自定义文件上传
document.querySelectorAll('.custom-file-upload').forEach((upload) => {
new CustomFileUpload(upload)
})
</script>
表单无障碍性
1. ARIA属性应用
html
<form role="form" aria-label="注册表单">
<div role="group" aria-labelledby="personal-info">
<h3 id="personal-info">个人信息</h3>
<label for="name">姓名</label>
<input type="text" id="name" aria-required="true" aria-invalid="false" />
<span role="alert" aria-live="polite"></span>
<label for="gender">性别</label>
<div role="radiogroup" aria-labelledby="gender">
<input type="radio" id="male" name="gender" value="male" />
<label for="male">男</label>
<input type="radio" id="female" name="gender" value="female" />
<label for="female">女</label>
</div>
</div>
</form>
2. 键盘导航优化
javascript
class KeyboardNavigation {
constructor(form) {
this.form = form
this.focusableElements = this.getFocusableElements()
this.setupEventListeners()
}
getFocusableElements() {
return Array.from(this.form.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))
}
setupEventListeners() {
this.form.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
this.handleTabNavigation(e)
}
})
}
handleTabNavigation(event) {
const currentIndex = this.focusableElements.indexOf(document.activeElement)
if (event.shiftKey) {
// 向后导航
if (currentIndex === 0) {
event.preventDefault()
this.focusableElements[this.focusableElements.length - 1].focus()
}
} else {
// 向前导航
if (currentIndex === this.focusableElements.length - 1) {
event.preventDefault()
this.focusableElements[0].focus()
}
}
}
}
// 初始化键盘导航
const form = document.querySelector('form')
new KeyboardNavigation(form)
动态表单处理
1. 条件字段显示
javascript
class DynamicForm {
constructor(form) {
this.form = form
this.setupConditionFields()
}
setupConditionFields() {
const triggers = this.form.querySelectorAll('[data-show-target]')
triggers.forEach((trigger) => {
trigger.addEventListener('change', () => {
const targetId = trigger.dataset.showTarget
const targetField = document.getElementById(targetId)
if (trigger.checked) {
targetField.style.display = 'block'
this.enableFields(targetField)
} else {
targetField.style.display = 'none'
this.disableFields(targetField)
}
})
})
}
enableFields(container) {
container.querySelectorAll('input, select, textarea').forEach((field) => (field.disabled = false))
}
disableFields(container) {
container.querySelectorAll('input, select, textarea').forEach((field) => (field.disabled = true))
}
}
2. 动态字段验证
javascript
class DynamicValidation {
constructor(form) {
this.form = form
this.validationRules = new Map()
this.errorMessages = new Map()
this.setupValidation()
}
// 添加验证规则
addValidationRule(fieldName, rule, message) {
if (!this.validationRules.has(fieldName)) {
this.validationRules.set(fieldName, [])
this.errorMessages.set(fieldName, [])
}
this.validationRules.get(fieldName).push(rule)
this.errorMessages.get(fieldName).push(message)
}
// 设置验证
setupValidation() {
this.form.addEventListener('submit', (e) => this.handleSubmit(e))
// 实时验证
this.form.querySelectorAll('input, select, textarea').forEach((field) => {
field.addEventListener('input', () => this.validateField(field))
field.addEventListener('blur', () => this.validateField(field))
})
}
// 验证单个字段
validateField(field) {
const fieldName = field.name
const rules = this.validationRules.get(fieldName)
const messages = this.errorMessages.get(fieldName)
if (!rules) return true
// 清除之前的错误
this.clearFieldError(field)
// 检查每个规则
for (let i = 0; i < rules.length; i++) {
const isValid = rules[i](field.value)
if (!isValid) {
this.showFieldError(field, messages[i])
return false
}
}
return true
}
// 处理表单提交
handleSubmit(event) {
event.preventDefault()
let isValid = true
// 验证所有字段
this.form.querySelectorAll('input, select, textarea').forEach((field) => {
if (!this.validateField(field)) {
isValid = false
}
})
if (isValid) {
// 表单验证通过,可以提交
console.log('表单验证通过')
}
}
// 显示字段错误
showFieldError(field, message) {
field.classList.add('invalid')
// 创建或更新错误消息元素
let errorElement = field.nextElementSibling
if (!errorElement || !errorElement.classList.contains('error-message')) {
errorElement = document.createElement('div')
errorElement.classList.add('error-message')
field.parentNode.insertBefore(errorElement, field.nextSibling)
}
errorElement.textContent = message
}
// 清除字段错误
clearFieldError(field) {
field.classList.remove('invalid')
const errorElement = field.nextElementSibling
if (errorElement && errorElement.classList.contains('error-message')) {
errorElement.remove()
}
}
}
// 使用示例
const form = document.querySelector('#registration-form')
const validation = new DynamicValidation(form)
// 添加验证规则
validation.addValidationRule('username', (value) => value.length >= 3, '用户名至少需要3个字符')
validation.addValidationRule('email', (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), '请输入有效的邮箱地址')
validation.addValidationRule('password', (value) => /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/.test(value), '密码至少8个字符,包含字母和数字')
// 添加CSS样式
const style = document.createElement('style')
style.textContent = `
.invalid {
border-color: #ff4444;
}
.error-message {
color: #ff4444;
font-size: 0.8em;
margin-top: 4px;
}
`
document.head.appendChild(style)
这个动态字段验证类提供了以下功能:
- 灵活的验证规则添加机制
- 实时验证和提交验证
- 清晰的错误消息显示
- 支持多个验证规则
- 自动样式处理
通过这种方式,我们可以根据需要动态添加和管理表单验证规则,同时保持代码的可维护性和可扩展性。