想起来前段时间面试时遇到的问题,说是如何用javascript实现一个表格控件,说说思路,当时还不知道该怎么办,只是说使用Ext中的表格控件,被追问不使用第三方控件时如何实现就傻眼了,这篇文章介绍的非常详细,搬过来共享。

数据表格控件的基础功能

数据表格是一个很常用的控件,用于把多列数据展示成表格的形状,通常有表头,表头可固定,表格内容可滚动。本文以一个数据表格控件为例,说明从构思到实现控件的整个过程。

为了使初学者更容易理解其中的原理,我们不使用任何额外的库,比如jQuery之类,仅仅使用bootstrap来控制样式。

1. 功能分析

DataGrid控件主要有以下几个功能: - 加载数据并展示成表格的形状 - 新增一行 - 删除一行 - 点击某行选中 - 修改行数据并刷新

DataGrid控件主要需要响应这样几个事件: - 加载完成 - 选中行变更

2. 实现原理

想要实现DataGrid控件,我们有三个步骤要做:

  • 用什么样的DOM结构来展现
  • 用什么样的结构来定义数据
  • 数据跟DOM结构如何关联起来

下面我们来考虑如何分别实现这三个步骤。

2.1. DOM结构

做一个控件之前,我们首先要把DOM结构确定下来,也就是用HTML能够展现控件的形态。什么结构适合展现数据表格呢?毫无疑问,是HTML中的table,语义上非常符合,为了省事,我们不考虑样式,直接用bootstrap中的表格样式。

<table class="table table-bordered table-striped"><thead><tr><th>#</th><th>Name</th><th>Gender</th><th>Age</th></tr></thead><tbody><tr><td>1</td><td>Tom</td><td>Male</td><td>5</td></tr><tr><td>2</td><td>Jerry</td><td>Female</td><td>2</td></tr><tr><td>3</td><td>Sun Wukong</td><td>Male</td><td>1024</td></tr></tbody>
</table>

这个结构足够表达DataGrid了,样子也还可以,所以我们很满意,开始考虑数据了。单使用这个结构,是难以做到表头固定,表格体可滚动的,但为了简单,我们先用这个结构来做。

2.2. 数据结构

在数据表格中,有两个数据是需要传入的,一是标题的列头数据,二是表格的内容,它们都很适合用数组来描述。

列头最少需要描述这些内容: 标题 数据字段

表格行需要有这些信息: 每个key的值

所以,对照上面的表格,我们可以把数据描述起来:

var columns = [{label: "#",field: "index"
}, {label: "Name",field: "name"
}, {label: "Gender",field: "gender"
}, {label: "Age",field: "age"
}];var data = [{index: 1,name: "Tom",gender: "Male",age: 5
}, {index: 2,name: "Jerry",gender: "Female",age: 2
}, {index: 3,name: "Sun Wukong",gender: "Male",age: 1024
}];

2.3. DOM和数据的关联

这部分听起来有些复杂,我先打个比方吧。

有一个勤劳的妈妈,她有三个宝宝,每个宝宝都有不少衣物,妈妈的职责是管理这些衣物,并且用它们来装扮宝宝们。这些衣物可以分为上衣,裤子,鞋子,袜子,帽子等等。

想象一下她是怎么做的: 把第一个宝宝抱过来,选几件衣服,给他穿上,放他出去玩。 对第二个宝宝做同样的操作。 对第三个宝宝做同样的操作。

我们甚至还可以推而广之,不管她有多少宝宝,都必定是按照这个方式做的。

那么,对比我们的控件,每条数据都是一个宝宝,把数据渲染到DOM的过程就好比给宝宝穿衣服。我们的列信息就好比衣物的分类。

我们有另外的问题: 宝宝们都出去玩了,我们问妈妈:最大的宝宝穿的是些什么衣服?绿色有大嘴猴的那件T恤穿在哪个宝宝身上?

这就要求妈妈对衣服和宝宝的关联关系有所记录。拿我们这个控件来看,宝宝就相当于每行的数据,衣服相当于用来展示的DOM节点,在这两者之间是需要一些关联的。

3. 程序设计

3.1. 实体划分

在我们这个控件中,存在两个实体:数据表格,表格行。

数据表格的职责很清楚,表格行的存在又是为了什么呢?我们完全可以把职责全部掌控在数据表格里,不下放给行。

设想我们要取选中行的name属性,我们的写法是这样的:

var row = grid.selectedRow();
var name = grid.get(row, "name");

希望这么写:

var row = grid.selectedRow();
var name = row.get("name");

甚至我们可以连着写:

var name = grid.selectedRow().get("name");

这里的问题是,selectedRow究竟要返回什么结构,才能让它有get方法?

这里有两种选择,一种是返回行的DOM结构,get方法附加在行上,行的数据也作为属性附加在DOM上,这样我们是没有单独的表格行实体的。另一种是创建表格行的实体,在其中管理DOM和数据的关联关系。这两种方式,我们应当如何选择呢?

把过多数据附加到DOM上并不是一个好的选择,尤其我们不能确定用户给控件传哪些字段,万一跟DOM自身的冲突了,会很糟糕。所以我们选择自己来管理这个关系。

3.2. 表格行的职责

确定了这样的原则之后,我们来考虑表格行的职责。

表格行,它应当可以被选中,也可以被取消选中(这个操作不是通过点击选中状态的自身来完成,而是点击其他行,由表格控件来取消自己的选中),可以读写数据。

我们考虑一下表格行的选中要干些什么。首先,如果已经有选中的行了,要把那个的样式去掉,然后把选中的行指向当前的行,再把当前行的样式变成选中的颜色。

这些职责,我们来考虑一下,哪些属于表格,那些数据表格行。

表格行是否应当知道所在表格当前选中的行是谁?不应该,因为这跟你无关。你只要知道自己是不是被选中就行了,不要管闲事。所以,管理选中行这个事情应当给表格做,某行被点击了,他不该擅自作决定,比如先把自己颜色变掉之类的,而是应当先请示汇报:“老大,有人翻我牌子,你把我选起来吧。”

严格来说,小弟不该干涉老大的工作,比如老大这时候就应该扇他一巴掌:“扑街仔,翻你牌几啦?不把老大放眼里啦?机不机到德墨忒尔法则啦?”然后还是把他选中。问题出在哪里呢?你多嘴了。你告诉老大,有人点我就可以了,你管老大后面干什么?那是他的事,虽然你知道老大要这么干,但你这个属于知道得太多,该打。

好了,现在老大知道有人翻你牌子了,拿了个本子翻了翻,把今天的头牌改成了你,然后分别对新老两个头牌大喝一声:“浩南把你的表拿下来,山鸡戴上!”看到没有,小弟听到老大指示之后才能改变外观。

山鸡去台湾作出一番事业,从前人家叫他山鸡,回来之后,有人还想这么叫,浩南哥语重心长地纠正:“叫鸡哥!”从此大家都叫他鸡哥了。

综上所述,表格行有这几个职责:

  • 创建,做一些初始化的事情
  • 销毁,主要是行呗删除的时候把DOM和数据的引用去掉,这样浏览器可以做内存回收
  • 选中,改变样式为选中状态,比如山鸡戴上了三个表,从此成为了代表
  • 取消选中,改变样式为非选中状态,比如浩南把自己的表给了山鸡,自己就不是代表了
  • 设置属性,比如浩南把山鸡的称呼改成鸡哥
  • 获取属性,比如别人看到山鸡,打听一下就知道他是鸡哥

3.3. 表格的职责

在上面所有功能里去掉表格行的职责,就得到了表格的职责

  • 加载列头数据
  • 根据数据加载列表
  • 添加行
  • 删除行
  • 选中行

3.4. 如何实现自定义事件

什么是事件呢?本质上是一种异步的机制,打个比方说,你委托我做饭,说,做完饭给你打个电话,你先出去玩了。为什么是异步呢,因为你不在这里等我做完就走了,你也不关心我什么时候做得完,反正做好告诉你就是了。你在我这里监听了做饭完成事件,我做完之后,把这个事件派发一下,派发到你了,我的职责就完成了。

这么一看,我们的事情其实不复杂。我要对你提供什么呢:

  • 添加事件的监听,注意这里可能不止一个,有可能多个人同时来等着吃饭。
  • 移除事件的监听,这是为何?因为可能我没做完,你先给我打了电话,说别人约你吃饭,你不需要知道我是否做完了。
  • 当事情做完,通知所有监听方。

这些分析完,我们的代码就好写了:

//事件派发机制的实现
var EventDispatcher = {addEventListener: function(eventType, handler) {//事件的存储if (!this.eventMap) {this.eventMap = {};}//对每个事件,允许添加多个监听if (!this.eventMap[eventType]) {this.eventMap[eventType] = [];}//把回调函数放入事件的执行数组this.eventMap[eventType].push(handler);},removeEventListener: function(eventType, handler) {for (var i=0; i<this.eventMap[eventType].length; i++) {if (this.eventMap[eventType][i] === handler) {this.eventMap[eventType].splice(i, 1);break;}}},dispatchEvent: function(event) {var eventType = event.type;for (var i=0; i<this.eventMap[eventType].length; i++) {//把对当前事件添加的处理函数拿出来挨个执行this.eventMap[eventType][i](event);}}
};

除此之外,我们还需要写一个辅助方法,用于把事件机制附加到表格上:

//简单的对象属性复制,把源对象上的属性复制到自己身上,只复制一层
Object.prototype.extend = function(base) {for (var key in base) {if (base.hasOwnProperty(key)) {this[key] = base[key];}}return this;
};

4. 上述功能的实现

我们把前面这两段代码放置在一个util.js中,因为这些功能不仅仅在DataGrid中会用到,然后,再创建一个datagrid.js,内容如下:

//作为一个控件,它的容器必须传入
function DataGrid(element) {this.columns = [];this.rows = [];element.innerHTML = '<table class="table table-bordered table-striped"><thead><tr></tr></thead><tbody></tbody><table>';this.header = element.firstChild.tHead;this.tbody = element.firstChild.tBodies[0];this.selectedRow = null;
}DataGrid.prototype = {loadColumns: function (columns) {if (this.header.rows.length > 0) {this.header.removeChild(this.header.rows[0]);}var tr = this.header.insertRow(0);for (var i = 0; i < columns.length; i++) {var th = tr.insertCell(i);th.innerHTML = columns[i].label;}this.columns = columns;},loadData: function (data) {for (var i = 0; i < data.length; i++) {this.insertRow(data[i]);}//跟外面说一声,数据加载好了var event = {type: "loadCompleted",target: this};this.dispatchEvent(event);},insertRow: function (data) {var row = new DataRow(data, this);this.tbody.appendChild(row.dom);this.rows.push(row);var that = this;row.addEventListener("selected", function (event) {that.select(event.row);});//已经成功添加了新行var event = {type: "rowInserted",newRow: row,target: this};this.dispatchEvent(event);},removeRow: function (row) {if (row === this.selectedRow) {this.selectedRow = null;}this.tbody.removeChild(row.dom);row.destroy();for (var i = 0; i < this.rows.length; i++) {if (this.rows[i] == row) {this.rows.splice(i, 1);break;}}//已经移除var event = {type: "rowRemoved",target: this};this.dispatchEvent(event);},select: function (row) {var event = {type: "changed",target: this,oldRow: this.selectedRow,newRow: row};if (this.selectedRow) {this.selectedRow.select(false);}if (row) {row.select(true);}this.selectedRow = row;this.dispatchEvent(event);}
}.extend(EventDispatcher);function DataRow(data, grid) {this.data = data;this.grid = grid;this.create();
}DataRow.prototype = {create: function () {var row = document.createElement("tr");for (var i = 0; i < this.grid.columns.length; i++) {var cell = document.createElement("td");cell.innerHTML = this.data[this.grid.columns[i].field] || "";row.appendChild(cell);}this.dom = row;var that = this;row.onclick = function (event) {//通知上级,我被点了var newEvent = {type: "selected",target: that,row: that};that.dispatchEvent(newEvent);}},destroy: function () {this.dom = null;this.data = null;this.grid = null;},select: function (flag) {if (flag) {this.dom.className = "info";}else {this.dom.className = "";}},set: function (key, value) {this.data[key] = value;for (var i = 0; i < this.grid.columns.length; i++) {if (this.grid.columns[i].field === key) {this.dom.childNodes[i].innerHTML = value;break;}}},get: function (key) {return this.data[key];},refresh: function (data) {this.data = data;for (var i = 0; i < this.grid.columns.length; i++) {this.dom.childNodes[i].innerHTML = data[this.grid.columns[i].field] || "";}}
}.extend(EventDispatcher);

然后,我们为它创建一个测试页面,叫做datagrid.html,内容如下:

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>DataGrid</title><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="description" content="DataGrid"><meta name="author" content="xu.fei@outlook.com"><link href="http://twitter.github.io/bootstrap/assets/css/bootstrap.css" rel="stylesheet"/><script type="text/javascript" src="js/utils.js"></script><script type="text/javascript" src="js/datagrid.js"></script>
</head>
<body><div class="span12"><div class="header"><h3>Staff Management</h3></div><div><div id="grid1" class="row"></div><div class="row"><div class="header"><h4>Detail</h4></div><hr/><form class="form-horizontal span6"><div class="control-group"><label class="control-label" for="inputIndex">Index</label><div class="controls"><input type="text" id="inputIndex" placeholder="Index"></div></div><div class="control-group"><label class="control-label" for="inputGender">Gender</label><div class="controls"><input type="text" id="inputGender" placeholder="Gender"></div></div></form><form class="form-horizontal span6"><div class="control-group"><label class="control-label" for="inputName">Name</label><div class="controls"><input type="text" id="inputName" placeholder="Name"></div></div><div class="control-group"><label class="control-label" for="inputAge">Age</label><div class="controls"><input type="text" id="inputAge" placeholder="age"></div></div></form></div></div><div class="modal-footer"><div id="operateBtns"><button class="btn btn-primary" οnclick="newClicked()">New</button><button class="btn btn-primary" οnclick="modifyClicked()"><i class="icon-edit icon-white"></i>Modify</button><button class="btn btn-primary" οnclick="deleteClicked()"><i class="icon-remove icon-white"></i>Delete</button></div><div id="confirmBtns"><button class="btn btn-primary" οnclick="okClicked()"><i class="icon-ok icon-white"></i>OK</button><button class="btn btn-primary" οnclick="cancelClicked()">Cancel</button></div></div></div><script type="text/javascript">var state = "View";var grid = new DataGrid(document.getElementById("grid1"));grid.addEventListener("loadCompleted", function(event) {if (event.target.rows.length > 0) {event.target.select(event.target.rows[0]);}});grid.addEventListener("changed", function(event) {var data;if (event.newRow) {data = event.newRow.data;}else {data = {};}setFormData(data);});grid.addEventListener("rowInserted", function(event) {event.target.select(event.newRow);});grid.addEventListener("rowRemoved", function(event) {if (event.target.rows.length > 0) {event.target.select(event.target.rows[0]);}});init();function init() {enableForm(false);switchButtons("Operate");var columns = [{label: "#",field: "index"}, {label: "Name",field: "name"}, {label: "Gender",field: "gender"}, {label: "Age",field: "age"}];var data = [{index: 1,name: "Tom",gender: "Male",age: 5}, {index: 2,name: "Jerry",gender: "Female",age: 2}, {index: 3,name: "Sun Wukong",gender: "Male",age: 1024}];grid.loadColumns(columns);grid.loadData(data);}function newClicked() {state = "New";switchButtons("Confirm");enableForm(true);setFormData({});}function modifyClicked() {state = "Modify";switchButtons("Confirm");enableForm(true);}function deleteClicked() {if (confirm("Sure?")) {grid.removeRow(grid.selectedRow);}}function okClicked() {var data = {index: document.getElementById("inputIndex").value,name: document.getElementById("inputName").value,gender: document.getElementById("inputGender").value,age: document.getElementById("inputAge").value};if (state === "New") {grid.insertRow(data);}else if (state === "Modify") {grid.selectedRow.refresh(data);}state = "View";switchButtons("Operate");enableForm(false);}function cancelClicked() {state = "View";switchButtons("Operate");enableForm(false);setFormData(grid.selectedRow.data);}function enableForm(flag) {document.getElementById("inputIndex").disabled = !flag;document.getElementById("inputName").disabled = !flag;document.getElementById("inputGender").disabled = !flag;document.getElementById("inputAge").disabled = !flag;}function switchButtons(group) {if (group === "Operate") {document.getElementById("operateBtns").style.display = "";document.getElementById("confirmBtns").style.display = "none";}else if (group === "Confirm") {document.getElementById("operateBtns").style.display = "none";document.getElementById("confirmBtns").style.display = "";}}function getFormData() {return {index: document.getElementById("inputIndex").value,name: document.getElementById("inputName").value,gender: document.getElementById("inputGender").value,age: document.getElementById("inputAge").value};}function setFormData(data) {document.getElementById("inputIndex").value = data.index || "";document.getElementById("inputName").value = data.name || "";document.getElementById("inputGender").value = data.gender || "";document.getElementById("inputAge").value = data.age || "";}</script>
</body>
</html>

我们可以看到,这已经可以跑一个简单的维护界面了,但我们的功能还是有限的,在后续篇幅中,我们会讲述如何实现表格的渲染器、改变列的宽度,固定列头,表格体滚动,排序等高级功能。我们的最终目标是:一个很正式的DataGrid控件。在线演示地址


写的非常清楚,但遗憾的是没有找到后续的篇幅。

转载于:https://www.cnblogs.com/ronaldhan/p/3287392.html

从零开始使用JavaScript编写数据表格控件(转载)相关推荐

  1. ajax17,17个使用AJAX技术的数据表格控件

    为了将数据高效的展示出来,数据需要有合适的结构,展现结构设计的越好,用户就越容易理解信息的内容.因此,提高数据的可萃取性和可扫描性非常有意义. 表,是一个非常好的数据呈现结构,非常便于人们分析和理解二 ...

  2. Xceed WinForm数据表格控件Xceed Grid For .NET详细介绍

    2019独角兽企业重金招聘Python工程师标准>>> Xceed Grid For .NET是一款高级的,多功能的.扩展性极强的数据表格控件,具有分组.主从表.多种主题外观.固定列 ...

  3. Tabulator javascript data grid数据表格控件v5.1.0 发布说明

    发行说明 编辑器配置 文件导入 阿贾克斯 键绑定 菜单 分页 列 可移动行 排序 格式化 数据树 下载 Bug修复 编辑器配置 一个新的.editorconfig文件已添加到 Tabulator 项目 ...

  4. Tabulator javascript data grid数据表格控件检测已修改行数据

    Tabulator 可以通过table.getEditedCells()获得编辑修改过的单元格,在通过单元格的getRow()函数获得行对象,让后通过行对象的.getData()函数获得编辑修改过行数 ...

  5. matlab 表格控件,[转载]matlab读取excel数据并显示在excel(activex控件)中

    figure; spreadsheet = actxcontrol('OWC11.Spreadsheet.11',[0 0 560 420]); M = {'abc', 123; 12.17, log ...

  6. VB表格控件总览与例程分析

    一.表格控件总览: 在VB开发环境中,表格控件在界面开发元素中占有重要的地位.它不仅有外观整洁.表达形式规范的优点,而且更重要的是它较高的信息表现率(就是相对于其他控件来说能够表达更多的信息),随着信 ...

  7. SpreadJS 纯前端表格控件应用案例:金融业数据智能分析平台

    由北京海创研发的金融业数据智能分析平台,通过嵌入 SpreadJS,实现了基于Web的数据采集.自助式报表设计与浏览.灵活查询.自主分析和数据预测等功能,简洁.易用,有效解决了用户数据填报.查询和分析 ...

  8. 分享12款 JavaScript 表格控件(DataGrid)

    JavaScript 表格控件可以操作大数据集的 HTML 表格,提供各种功能,如分页.排序.过滤以及行编辑.在本文中,我们整理了13个最好的 JavaScript 表格插件分享给开发人员,开发者可以 ...

  9. js固定表格行列_纯前端表格控件SpreadJS V14.0发布:组件化编辑器+数据透视表

    SpreadJS 是一款基于 HTML5 的纯前端表格控件,兼容 450 种以上的 Excel 公式,具备"高性能.跨平台.与 Excel 高度兼容"的产品特性,可为用户提供高度类 ...

最新文章

  1. 这个Python知识点,90%初学者没太整明白
  2. springboot新版本(2.0.0+)自定义ErrorController中使用ErrorAttributes
  3. jmeter中没有sampler_jmeter(七)-BeanShell常用内置变量和场景
  4. 3COM小型办公室有线局域网方案
  5. Java中关于Arrays.sort的两种重载方法的理解
  6. c语言 地址+1,C语言中,为什么指针表达式的值+1.对应的地址值却+4?/为什么两个数组元素的地址相减之差不为地址之差?...
  7. 深信服上网管理设备基本操作
  8. Xception论文笔记
  9. Linux操作Oracle(13)——java执行sql查询语句 报错 “无法绑定由多个部分组成的标识符“ 解决方案
  10. 虚拟机系统iso镜像下载_微软Windows 10 Build 19013(20H1)简体中文版ISO镜像可供下载...
  11. Minecraft Forge 安装
  12. 天宫初级认证答案_百度初级认证考题_试题1题目+答案
  13. opencv如何隐藏窗口-cvNameWindow创建窗口的时候会创建两个窗口,一个主窗口,一个子窗口。
  14. 阿里巴巴国际站—产品运营工作台操作指南
  15. android别踩白块设计,别踩白块儿实例——按键精灵手机助手
  16. 【环境搭建】Docker镜像相关操作(切换镜像源、查询、获取、查看、创建、上传、保存、删除等)
  17. 当新消费撞上颜值经济,国产美瞳成为资本新宠儿
  18. 2021较全 腾讯云服务器安装宝塔面板建网站全过程(图文教程)
  19. mysql where 位子,mysql中 地里位置搜索 (抄)
  20. IOS开发学习周报(二)

热门文章

  1. 已解决windows开机时,系统提示此windows副本不是正版
  2. 句柄(Handler)到底是什么?
  3. 「构建企业级推荐系统系列」深度学习在推荐系统中的应用
  4. Word2016出现“此功能看似已中断 并需要修复”解决教程分享
  5. 今年计算机/软件等专业调剂信息集合!
  6. [转]为何我们这样一个“丑陋”的种族,能够屹立……
  7. linux root密码修改失败,【转】Linux root修改密码失败
  8. mysql里乘号怎么_如何在MySQL SELECT上将无符号整数乘以-1?
  9. 聊聊IM系统的即时性和可靠性
  10. 拜日式精准引导词_拜日式108遍的意义。