组件化可编辑表格
目录
实验内容
目标分析
最终成果
实现过程
1.搭建HTML框架
2.自定义数据
3.创建EditableTable类,处理配置合并。
4.将表格组件挂载到指定的 DOM 元素并触发渲染
5.renderTable()生成表格DOM结构
创建表头
创建表体
6.表格配置
7.挂载表格
知识总结
constructor构造函数
mount()
Object.keys(this.config.cols).forEach(...)
check函数:对用户输入的数据进行实时校验,确保其符合业务规则。
完整代码
实验内容
完成以数据驱动模式动态生成页面中可交互的数据表格,并可根据任意结构化表格数据和配置信息进行可编辑表格的生成。
将该实验的代码合理组织为通用的可编辑表格Web组件生成代码。
目标分析
1.可编辑表格
2.由数据驱动的模块化表格
最终成果
实现过程
1.搭建HTML框架
<div class="table-box">
<h2>成绩表</h2>
<table id="scoreTable" class="table"></table>
<h2>学生信息表</h2>
<table id="infoTable" class="table"></table>
</div>
2.自定义数据
"score":[
{"name":"张三","chinese":100,"math":90,"english":80},
{"name":"李四","chinese":90,"math":100,"english":80},
{"name":"王五","chinese":80,"math":80,"english":100}
],
"information":[
{"name":"张三","sex":"男","age":18,"address":"北京"},
{"name":"李四","sex":"男","age":19,"address":"上海"},
{"name":"王五","sex":"女","age":17,"address":"广州"},
{"name":"赵六","sex":"女","age":18,"address":"深圳"},
{"name":"孙七","sex":"男","age":19,"address":"杭州"},
{"name":"钱八","sex":"男","age":17,"address":"南京"}
]
3.创建EditableTable类,处理配置合并。
在初始化时保存传入的数据和配置对象,方便后续的方法使用。接下来,还有一个this.tableElement被初始化为null,会在其他方法中创建或引用一个表格DOM元素。
constructor(data, config) {
this.data = data;
this.config = config;
this.tableElement = null;
}
4.将表格组件挂载到指定的 DOM 元素并触发渲染
使用mount方法将组件挂载到指定地方。接收一个 DOM 元素element,将其赋值给this.tableElement。调用renderTable() 方法渲染表格内容到该元素中。
mount(element) {
this.tableElement = element;
this.renderTable();
}
5.renderTable()生成表格DOM结构
创建表头
创建一个一个thead元素和一个tr元素作为表头的行。然后遍历this.confug.cols的键,对每一个键创建一个th元素,设置其内容为对应的列的标题。将th元素添加到表头的行中,形成表头。同时 ,如果配置中canDelete为true,则额外添加一个“操作”列。
Object.keys(this.config.cols)获取所有列的键,然后遍历每个键,创建th元素,并设置其内容为对应列的标题,最后将th添加到headerRow中。
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
Object.keys(this.config.cols).forEach((key) => {
const th = document.createElement("th");
th.textContent = this.config.cols[key].title;
headerRow.appendChild(th);
});
if (this.config.canDelete) {
const operationTh = document.createElement("th");
operationTh.textContent = "操作";
headerRow.appendChild(operationTh);
}
thead.appendChild(headerRow);
table.appendChild(thead);
创建表体
创建了一个tbody元素,遍历this.data数组中的每个项目,为每个项目创建一行(tr)。对于每个数据项,遍历配置中的列(this.config.cols),根据列配置创建单元格(td)。如果列配置为可编辑(editable),则创建input元素,并在输入框失去焦点时(blur事件)更新数据。如果配置允许删除(canDelete),则在每行末尾添加一个删除按钮,点击按钮会从数据中删除该行并重新渲染表格。
const tbody = document.createElement("tbody");
this.data.forEach((item, index) => {
const row = document.createElement("tr");
Object.keys(this.config.cols).forEach((key) => {
const td = document.createElement("td");
const configCol = this.config.cols[key];
if (configCol.editable) {
const input = document.createElement("input");
input.value = item[key];
input.addEventListener("blur", (e) => {
const val = e.target.value;
if (configCol.check) {
const isValid = configCol.check(val, e.target);
if (isValid) {
const newValue =
configCol.type === "number" ? parseFloat(val) : val;
item[key] = newValue;
}
} else {
item[key] = val;
}
});
td.appendChild(input);
} else {
td.textContent = item[key];
}
row.appendChild(td);
});
if (this.config.canDelete) {
const deleteTd = document.createElement("td");
const deleteButton = document.createElement("button");
deleteButton.textContent = "删除";
deleteButton.addEventListener("click", () => {
this.data.splice(index, 1);
this.renderTable();
});
deleteTd.appendChild(deleteButton);
row.appendChild(deleteTd);
}
tbody.appendChild(row);
});
table.appendChild(tbody);
6.表格配置
通用属性:
- tableCSS:表格CSS类名'table'。
- canDelete:是否允许删除行
- cols:列配置对象,键为列标识符。
配置项目 | 成绩表 | 信息表 |
---|---|---|
列类型 | 数值型 | 混合型 |
校验规则 | 数值范围(0-100) | 性别枚举、年龄整数、地址非空 |
错误反馈 | 通过error类名高亮输入框,提示用户错误 | 通过error类名高亮输入框,提示用户错误 |
check函数验证输入数据的有效性。
// 成绩表配置
const scoreConfig = {
tableCSS: "table",
canDelete: true,
cols: {
name: { title: "姓名", editable: false },
chinese: {
title: "语文",
type: "number",
editable: true,
check: (val, target) => {
const isValid = !isNaN(val) && val >= 0 && val <= 100;
target.classList.toggle("error", !isValid);
return isValid;
},
},
math: {
title: "数学",
type: "number",
editable: true,
check: (val, target) => {
const isValid = !isNaN(val) && val >= 0 && val <= 100;
target.classList.toggle("error", !isValid);
return isValid;
},
},
english: {
title: "英语",
type: "number",
editable: true,
check: (val, target) => {
const isValid = !isNaN(val) && val >= 0 && val <= 100;
target.classList.toggle("error", !isValid);
return isValid;
},
},
},
};
// 信息表配置
const infoConfig = {
tableCSS: "table",
canDelete: true,
cols: {
name: { title: "姓名", editable: false },
sex: {
title: "性别",
editable: true,
check: (val, target) => {
const isValid = ["男", "女"].includes(val);
target.classList.toggle("error", !isValid);
return isValid;
},
},
age: {
title: "年龄",
type: "number",
editable: true,
check: (val, target) => {
const isValid = Number.isInteger(Number(val)) && val > 0;
target.classList.toggle("error", !isValid);
return isValid;
},
},
address: {
title: "地址",
editable: true,
check: (val, target) => {
const isValid = val.trim().length > 0;
target.classList.toggle("error", !isValid);
return isValid;
},
},
},
};
7.挂载表格
创建了两个EditableTable实例,分别挂载到不同的DOM元素上。
new EditableTable(scoreData, scoreConfig).mount(
document.getElementById("scoreTable")
);
new EditableTable(informationData, infoConfig).mount(
document.getElementById("infoTable")
);
知识总结
constructor构造函数
constructor是一种用于创建和初始化class对象实例的特殊方法。一个类不能有一个以上的constructor方法。
参考网址:构造函数 - JavaScript | MDN
mount()
mount() 是类MyComponent的一个方法(本质是一个函数),需要通过实例调用。
const component = new MyComponent();
component.mount(document.getElementById("container"));
Object.keys(this.config.cols).forEach(...)
Object.keys(this.config.cols).forEach(...)是一种常见的遍历对象键的方式。使用Object.keys可以将对象的键转换为数组,然后使用数组的forEach方法进行遍历。
check函数:对用户输入的数据进行实时校验,确保其符合业务规则。
优点:
- 每个列可自定义校验规则(如数字范围、枚举值、必填项)。
- 校验规则与列配置绑定。
- 即时反馈输入错误。
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>信息表</title>
<style>
.table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.table th,
.table td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}
.table th {
background-color: #f2f2f2;
}
.error {
background-color: crimson;
}
input {
border: none;
}
</style>
</head>
<body>
<div class="table-box">
<h2>成绩表</h2>
<table id="scoreTable" class="table"></table>
<h2>学生信息表</h2>
<table id="infoTable" class="table"></table>
</div>
<script>
class EditableTable {
constructor(data, config) {
this.data = data;
this.config = config;
this.tableElement = null;
}
mount(element) {
this.tableElement = element;
this.renderTable();
}
renderTable() {
const table = this.tableElement;
table.innerHTML = "";
// 创建表头
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
Object.keys(this.config.cols).forEach((key) => {
const th = document.createElement("th");
th.textContent = this.config.cols[key].title;
headerRow.appendChild(th);
});
if (this.config.canDelete) {
const operationTh = document.createElement("th");
operationTh.textContent = "操作";
headerRow.appendChild(operationTh);
}
thead.appendChild(headerRow);
table.appendChild(thead);
// 创建表体
const tbody = document.createElement("tbody");
this.data.forEach((item, index) => {
const row = document.createElement("tr");
Object.keys(this.config.cols).forEach((key) => {
const td = document.createElement("td");
const configCol = this.config.cols[key];
if (configCol.editable) {
const input = document.createElement("input");
input.value = item[key];
input.addEventListener("blur", (e) => {
const val = e.target.value;
if (configCol.check) {
const isValid = configCol.check(val, e.target);
if (isValid) {
const newValue =
configCol.type === "number" ? parseFloat(val) : val;
item[key] = newValue;
}
} else {
item[key] = val;
}
});
td.appendChild(input);
} else {
td.textContent = item[key];
}
row.appendChild(td);
});
if (this.config.canDelete) {
const deleteTd = document.createElement("td");
const deleteButton = document.createElement("button");
deleteButton.textContent = "删除";
deleteButton.addEventListener("click", () => {
this.data.splice(index, 1);
this.renderTable();
});
deleteTd.appendChild(deleteButton);
row.appendChild(deleteTd);
}
tbody.appendChild(row);
});
table.appendChild(tbody);
}
}
// 成绩数据
const scoreData = [
{ name: "张三", chinese: 100, math: 90, english: 80 },
{ name: "李四", chinese: 90, math: 100, english: 80 },
{ name: "王五", chinese: 80, math: 80, english: 100 },
];
// 学生信息数据
const informationData = [
{ name: "张三", sex: "男", age: 18, address: "北京" },
{ name: "李四", sex: "男", age: 19, address: "上海" },
{ name: "王五", sex: "女", age: 17, address: "广州" },
{ name: "赵六", sex: "女", age: 18, address: "深圳" },
{ name: "孙七", sex: "男", age: 19, address: "杭州" },
{ name: "钱八", sex: "男", age: 17, address: "南京" },
];
// 成绩表配置
const scoreConfig = {
tableCSS: "table",
canDelete: true,
cols: {
name: { title: "姓名", editable: false },
chinese: {
title: "语文",
type: "number",
editable: true,
check: (val, target) => {
const isValid = !isNaN(val) && val >= 0 && val <= 100;
target.classList.toggle("error", !isValid);
return isValid;
},
},
math: {
title: "数学",
type: "number",
editable: true,
check: (val, target) => {
const isValid = !isNaN(val) && val >= 0 && val <= 100;
target.classList.toggle("error", !isValid);
return isValid;
},
},
english: {
title: "英语",
type: "number",
editable: true,
check: (val, target) => {
const isValid = !isNaN(val) && val >= 0 && val <= 100;
target.classList.toggle("error", !isValid);
return isValid;
},
},
},
};
// 信息表配置
const infoConfig = {
tableCSS: "table",
canDelete: true,
cols: {
name: { title: "姓名", editable: false },
sex: {
title: "性别",
editable: true,
check: (val, target) => {
const isValid = ["男", "女"].includes(val);
target.classList.toggle("error", !isValid);
return isValid;
},
},
age: {
title: "年龄",
type: "number",
editable: true,
check: (val, target) => {
const isValid = Number.isInteger(Number(val)) && val > 0;
target.classList.toggle("error", !isValid);
return isValid;
},
},
address: {
title: "地址",
editable: true,
check: (val, target) => {
const isValid = val.trim().length > 0;
target.classList.toggle("error", !isValid);
return isValid;
},
},
},
};
// 挂载表格
new EditableTable(scoreData, scoreConfig).mount(
document.getElementById("scoreTable")
);
new EditableTable(informationData, infoConfig).mount(
document.getElementById("infoTable")
);
</script>
</body>
</html>