使用 HTML + JavaScript 实现表单自动保存
文章目录
一、表单自动保存
用户在填写表单时,经常会遇到意外关闭页面或浏览器崩溃的情况,导致长时间的输入成果丢失。通过实现表单自动保存功能,可以在用户输入时实时保存数据到本地存储,当用户意外离开后重新进入页面时,能够恢复之前填写的内容,大大提升用户体验。这种机制特别适用于注册表单、问卷调查、订单填写等需要用户投入时间的场景。本文将介绍如何使用 HTML、CSS 和 JavaScript 实现表单自动保存功能。
二、效果演示
当用户在表单中进行输入操作时,系统会在用户停止输入 1 秒后自动将当前填写的数据保存到浏览器的本地存储中。页面右上角会显示一个"已自动保存"的提示信息,提醒用户数据已保存。当用户刷新页面或重新访问该页面时,之前填写的数据会自动恢复到相应的表单控件中。即使用户意外关闭浏览器后再次打开,数据依然存在。用户可以通过提交按钮提交表单,或使用清空按钮清除表单内容和本地存储。
三、系统分析
1.页面结构
页面主要包括以下几个区域:
1.1 表单主体区域
包含各种类型的表单控件,如文本输入框、日期选择器、单选按钮组、复选框组、下拉选择器和文本域。
<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" name="name" placeholder="请输入您的姓名">
</div>
<div class="form-group">
<label for="birthdate">出生日期</label>
<input type="date" id="birthdate" name="birthdate">
</div>
<!-- 更多表单元素... -->
1.2 操作按钮区域
包含提交表单和清空表单的按钮。
<div class="buttons">
<button type="submit" class="btn-primary">提交表单</button>
<button type="button" class="btn-secondary" onclick="clearForm()">清空表单</button>
</div>
1.3 提示信息区域
位于页面右上角,用于显示自动保存状态的提示信息。
<div id="saveIndicator" class="save-indicator">已自动保存</div>
2、核心功能实现
2.1 自动保存类初始化
创建 AutoSaveForm 类,用于管理表单的自动保存功能。在构造函数中初始化必要的 DOM 元素引用,并在 init 方法中设置事件监听器。主要监听 input 和 change 事件来检测用户输入,同时监听页面可见性变化以在用户切换标签页时保存数据。
class AutoSaveForm {
constructor(formId, storageKey = 'autoSaveForm') {
this.form = document.getElementById(formId);
this.storageKey = storageKey;
this.saveIndicator = document.getElementById('saveIndicator');
this.status = document.getElementById('status');
this.saveTimeout = null;
this.init();
}
init() {
this.loadSavedData();
this.form.addEventListener('input', (e) => this.handleInput(e));
this.form.addEventListener('change', (e) => this.handleInput(e));
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') this.saveData();
});
}
}
2.2 输入事件处理与延迟保存
当用户输入时,需要避免频繁保存造成性能问题。通过 handleInput 方法实现防抖机制,每次输入后设置 1 秒的延迟,如果在延迟期间又有新的输入,则清除之前的定时器并重新设置,确保只在用户停止输入后才执行保存操作。
handleInput(e) {
if (this.saveTimeout) clearTimeout(this.saveTimeout);
this.saveTimeout = setTimeout(() => this.saveData(), 1000);
}
2.3 数据收集与本地存储
saveData 方法负责收集表单中的所有数据并保存到 localStorage。对于复选框组,需要特殊处理以正确收集所有选中的值。同时处理未选中的复选框,确保它们的值也被正确记录。
saveData() {
const data = {};
const formData = new FormData(this.form);
for (let [key, value] of formData.entries()) {
const elements = this.form.querySelectorAll(`input[name="${key}"][type="checkbox"]`);
if (elements.length > 1) {
if (data[key]) {
if (!Array.isArray(data[key])) data[key] = [data[key]];
data[key].push(value);
} else {
data[key] = [value];
}
} else {
data[key] = value;
}
}
this.form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
if (!formData.has(checkbox.name)) {
const groupCheckboxes = this.form.querySelectorAll(`input[type="checkbox"][name="${checkbox.name}"]`);
if (groupCheckboxes.length > 1) {
if (!data[checkbox.name]) data[checkbox.name] = [];
} else {
data[checkbox.name] = false;
}
}
});
localStorage.setItem(this.storageKey, JSON.stringify(data));
this.showSaveIndicator();
}
2.4 数据恢复与状态显示
loadSavedData 方法从本地存储中读取之前保存的数据,并将其恢复到相应的表单控件中。针对不同类型的表单元素(文本框、单选框、复选框)使用不同的恢复逻辑。showSaveIndicator 方法控制保存状态提示信息的显示和隐藏。
loadSavedData() {
const savedData = localStorage.getItem(this.storageKey);
if (savedData) {
const data = JSON.parse(savedData);
Object.keys(data).forEach(key => {
const elements = this.form.elements[key];
if (elements) {
if (elements.type === 'checkbox' || elements[0]?.type === 'checkbox') {
this.setCheckboxValues(elements, data[key]);
} else if (elements.type === 'radio' || elements[0]?.type === 'radio') {
this.setRadioValues(elements, data[key]);
} else if (Array.isArray(data[key]) && elements.length) {
Array.from(elements).forEach(el => el.checked = data[key].includes(el.value));
} else {
elements.value = data[key];
}
}
});
}
}
四、完整代码
git地址:https://gitee.com/ironpro/hjdemo/blob/master/form-autosave/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表单自动保存</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #f5f5f5;
padding: 20px;
min-height: 100vh;
color: #333;
}
.container {
background: white;
padding: 20px;
width: 800px;
margin: 0 auto;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
h1 {
color: #2c3e50;
font-size: 22px;
margin-bottom: 25px;
text-align: center;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
}
.form-group {
margin-bottom: 20px;
display: flex;
flex-direction: row;
align-items: flex-start;
}
.form-group.checkbox-single {
align-items: center;
}
label {
display: inline-block;
margin-bottom: 0;
color: #374151;
font-size: 14px;
font-weight: 500;
width: 120px;
flex-shrink: 0;
}
.form-group.checkbox-single label {
width: auto;
margin-left: 8px;
}
input[type="text"], input[type="email"], input[type="number"], input[type="date"], textarea, select {
width: calc(100% - 130px);
padding: 10px 12px;
border: 1px solid #d1d5db;
font-size: 14px;
transition: border-color 0.2s;
background: white;
}
.form-group.checkbox-single input[type="checkbox"] {
width: auto;
}
input[type="text"]:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="date"]:focus, textarea:focus, select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, .1);
}
textarea {
resize: vertical;
min-height: 100px;
}
.radio-group, .checkbox-group, .checkbox-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
width: calc(100% - 130px);
}
.radio-item, .checkbox-item {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 6px 0;
flex-shrink: 0;
}
input[type="radio"], input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: #3b82f6;
}
.save-indicator {
position: fixed;
top: 16px;
right: 16px;
background: #10b981;
color: white;
padding: 8px 16px;
border-radius: 4px;
font-size: 12px;
opacity: 0;
transform: translateY(-10px);
transition: all 0.2s ease;
pointer-events: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
}
.save-indicator.show {
opacity: 1;
transform: translateY(0);
}
.buttons {
display: flex;
gap: 12px;
margin-top: 24px;
}
button {
padding: 10px 20px;
border: 1px solid transparent;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-secondary {
background: white;
color: #374151;
border: 1px solid #d1d5db;
}
.btn-secondary:hover {
background: #f3f4f6;
}
</style>
</head>
<body>
<div class="container">
<h1>表单自动保存</h1>
<form id="autoSaveForm">
<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" name="name" placeholder="请输入您的姓名">
</div>
<div class="form-group">
<label for="birthdate">出生日期</label>
<input type="date" id="birthdate" name="birthdate">
</div>
<div class="form-group">
<label>性别</label>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="male" name="gender" value="male">
<label for="male">男</label>
</div>
<div class="radio-item">
<input type="radio" id="female" name="gender" value="female">
<label for="female">女</label>
</div>
</div>
</div>
<div class="form-group">
<label>兴趣爱好</label>
<div class="checkbox-grid">
<div class="checkbox-item">
<input type="checkbox" id="reading" name="hobbies" value="reading">
<label for="reading">阅读</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="sports" name="hobbies" value="sports">
<label for="sports">运动</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="music" name="hobbies" value="music">
<label for="music">音乐</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="travel" name="hobbies" value="travel">
<label for="travel">旅行</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cooking" name="hobbies" value="cooking">
<label for="cooking">烹饪</label>
</div>
</div>
</div>
<div class="form-group">
<label>技能水平</label>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="beginner" name="skill" value="beginner">
<label for="beginner">初学者</label>
</div>
<div class="radio-item">
<input type="radio" id="intermediate" name="skill" value="intermediate">
<label for="intermediate">中级</label>
</div>
<div class="radio-item">
<input type="radio" id="advanced" name="skill" value="advanced">
<label for="advanced">高级</label>
</div>
</div>
</div>
<div class="form-group">
<label for="city">城市</label>
<select id="city" name="city">
<option value="">请选择城市</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="guangzhou">广州</option>
<option value="shenzhen">深圳</option>
<option value="hangzhou">杭州</option>
<option value="chengdu">成都</option>
</select>
</div>
<div class="form-group">
<label>通知设置</label>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="emailNotify" name="notifications" value="email">
<label for="emailNotify">邮件通知</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="smsNotify" name="notifications" value="sms">
<label for="smsNotify">短信通知</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="pushNotify" name="notifications" value="push">
<label for="pushNotify">推送通知</label>
</div>
</div>
</div>
<div class="form-group">
<label for="message">留言</label>
<textarea id="message" name="message" placeholder="请输入您的留言"></textarea>
</div>
<div class="form-group checkbox-single">
<input type="checkbox" id="agree" name="agree">
<label for="agree">我已阅读并同意服务条款</label>
</div>
<div class="buttons">
<button type="submit" class="btn-primary">提交表单</button>
<button type="button" class="btn-secondary" onclick="clearForm()">清空表单</button>
</div>
</form>
</div>
<div id="saveIndicator" class="save-indicator">已自动保存</div>
<script>
class AutoSaveForm {
constructor(formId, storageKey = 'autoSaveForm') {
this.form = document.getElementById(formId);
this.storageKey = storageKey;
this.saveIndicator = document.getElementById('saveIndicator');
this.status = document.getElementById('status');
this.saveTimeout = null;
this.init();
}
init() {
this.loadSavedData();
this.form.addEventListener('input', (e) => this.handleInput(e));
this.form.addEventListener('change', (e) => this.handleInput(e));
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') this.saveData();
});
}
handleInput(e) {
if (this.saveTimeout) clearTimeout(this.saveTimeout);
this.saveTimeout = setTimeout(() => this.saveData(), 1000);
}
saveData() {
const data = {};
const formData = new FormData(this.form);
for (let [key, value] of formData.entries()) {
const elements = this.form.querySelectorAll(`input[name="${key}"][type="checkbox"]`);
if (elements.length > 1) {
if (data[key]) {
if (!Array.isArray(data[key])) data[key] = [data[key]];
data[key].push(value);
} else {
data[key] = [value];
}
} else {
data[key] = value;
}
}
this.form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
if (!formData.has(checkbox.name)) {
const groupCheckboxes = this.form.querySelectorAll(`input[type="checkbox"][name="${checkbox.name}"]`);
if (groupCheckboxes.length > 1) {
if (!data[checkbox.name]) data[checkbox.name] = [];
} else {
data[checkbox.name] = false;
}
}
});
localStorage.setItem(this.storageKey, JSON.stringify(data));
this.showSaveIndicator();
}
loadSavedData() {
const savedData = localStorage.getItem(this.storageKey);
if (savedData) {
const data = JSON.parse(savedData);
Object.keys(data).forEach(key => {
const elements = this.form.elements[key];
if (elements) {
if (elements.type === 'checkbox' || elements[0]?.type === 'checkbox') {
this.setCheckboxValues(elements, data[key]);
} else if (elements.type === 'radio' || elements[0]?.type === 'radio') {
this.setRadioValues(elements, data[key]);
} else if (Array.isArray(data[key]) && elements.length) {
Array.from(elements).forEach(el => el.checked = data[key].includes(el.value));
} else {
elements.value = data[key];
}
}
});
}
}
setCheckboxValues(elements, savedValue) {
if (elements.length) {
Array.from(elements).forEach(checkbox => {
if (Array.isArray(savedValue)) checkbox.checked = savedValue.includes(checkbox.value);
else checkbox.checked = savedValue === true;
});
} else {
elements.checked = savedValue === true || elements.value === savedValue;
}
}
setRadioValues(elements, savedValue) {
if (elements.length) Array.from(elements).forEach(radio => radio.checked = radio.value === savedValue);
else elements.checked = elements.value === savedValue;
}
showSaveIndicator() {
this.saveIndicator.classList.add('show');
setTimeout(() => this.saveIndicator.classList.remove('show'), 2000);
}
handleSubmit(e) {
e.preventDefault();
this.saveData();
setTimeout(() => {
if (confirm('是否清空已保存的数据?')) {
this.clearSavedData();
this.form.reset();
}
}, 1000);
}
clearSavedData() {
localStorage.removeItem(this.storageKey);
}
}
const autoSaveForm = new AutoSaveForm('autoSaveForm');
function clearForm() {
if (confirm('确定要清空表单吗?')) {
autoSaveForm.clearSavedData();
document.getElementById('autoSaveForm').reset();
}
}
</script>
</body>
</html>
更多推荐
所有评论(0)