组件是一个可复用的类,UI组件应该把开发人员从组织HTML+CSS中解放出来,让他们只关注驱动UI显示的数据,以及各种业务逻辑,事件驱动等,之前的自建JS类库已经有了基本对象扩展、类扩展、以及最核心的功能:事件监听管理,今天我就来使用这些自建的JS方法来构建一个简单并且可扩展的Grid组件!
先说说Ext Grid吧,其功能极其强大,有着让人惊艳的UI显示和强大的交互功能,这里要制作的Grid组件当然不会要和Ext比较,况且涉及的知识点也会非常多难度也很大。我只是采用Ext GridView源码中的一种事件代理机制来设计自己的组件,再详细阐述事件代理机制前,我们先搭建Grid组件的基本类结构吧:
HL.ui.Grid = Class({
body : {},
// 组件初始化方法
init : function(config){
var id = config.id, //HL.serialID;
container = config.renderer,
cm = config.cm,
tpl;
this.ds = config.dataSource; // Grid需要展示的数据
HL.Observable(this); // 实现观察者事件监听接口
this.renderUI(id, container, cm);
this.initEvent();
},
// 根据数据生成Grid的DOM结构,渲染到页面中
renderUI : function(id, container, cm){
var i, len, record, c,
body = ['<table class=' + this.getTableCls() + '><thead><tr>'];
for(i = 0, len = cm.length; i < len; i++){
body.push(cm[i]);
}
for(i = 0, len = this.ds.data.length; i < len; i++){
for(c = 0; c < record.length; c++ ){
body.push(record[c]);
}
}
body.push('</tbody></table>');
this.body = document.createElement('div');
this.body.id = id;
this.body.innerHTML = body.join('');
document.getElementById(container).appendChild(this.body);
},
// 注册Grid的DOM事件
initEvent : function(){
HL.on(this.body, 'mousedown', this.onMouseDown, this);
HL.on(this.body, 'click', this.onClick, this);
HL.on(this.body, 'keydown', this.onKeyDown, this);
HL.on(this.body, 'mousemove', this.onMouseMove, this);
HL.on(this.body, 'mouseout', this.onMouseOut, this);
},
// 事件代理方法
processHandler : function(eventName, e){
},
onMouseDown : function(e){
this.processHandler('mousedown', e);
if(e.button == 2){ // 是否右键,是则触发右键点击事件
this.processHandler('contextmenu', e);
}
},
onClick : function(e){
this.processHandler('click', e);
},
onKeyDown : function(e){
this.processHandler('keydown', e);
},
onMouseMove : function(e){
if(this.currentEl !== e.getTarget()){
this.processHandler('mousemove', e);
}
},
onMouseOut : function(e){
this.clearHoverCls();
}
});
OK,这是Grid的基本类结构,根据之前Class方法,这个自定义类构造器中会调用init方法,所以Grid初始化时依次加载配置参数,实现之前实现的软件驱动事件接口,然后是渲染DOM结构,最后是给Grid注册各种事件监听。renderUI方法这里只写了点象征性的代码,因为构造Grid的DOM结构代码量确实比较大,姑且跳过这块让我先关注Grid的事件机制。
可以看到每个事件监听函数都调用了processHandler原形方法,这里我采用了Ext的设计理念,所有DOM事件都进行委托代理,processHandler是一个代理方法,它负责接收其他事件的类型和Event对象,然后根据触发事件的目标对象来实现更细粒度的操作,这么做的原因是:构造Grid DOM结构时,不可能逐个给每个 td tr 注册事件,效率底下代码不优雅是一方面,注册过多DOM事件影响性能也是一很大问题,下面是processHandler的代码:
processHandler : function(eventName, e){
var row, cell,
t = e.getTarget(),
head = this.getHeadIndex(t);
this.currentEl = t;
if(head !== false){
this.fire( 'head' + eventName, head, e);
} else {
row = this.getRowIndex(t);
if(row !== false){
this.fire( 'row' + eventName, row, e);
cell = this.getCellIndex(t, row);
if(cell !== false){
this.fire( 'cell' + eventName, row, cell, e);
}
}
}
}
target是实际触发事件的DOM对象,我们之前只是给Grid的容器对象注册事件,这里如果能知道Target是什么类型的对象,处于Grid的什么位置,我们就能实现更加复杂的功能。先判断是否是标题head,是则触发head的响应事件,传入列序号和Event对象,这是之前所说的软件驱动事件,关于观察者事件模型再做一个更详细的阐述:观察者模式是一个解耦性极强的设计模式,目标对象只需发布事件通知,其他对象只需监听自己注册的事件,两者之间毫无任何关联,这在功能组合上能实现很强的灵活性;
根据target所处的位置,依次触发标题、行、单元格的各种事件,比如双击某一单元格,则触发cellDbclick事件,执行相关业务逻辑。说完事件代理让我们最后看看如何实现Target的查找定位:
getHeadIndex : function(t){
var i, len,
h = HL.dom.findParent(t, 'th'),
th = this.body.querySelectorAll('tr th');
for(i = 0, len = th.length; i < len; i++){
if(th[i] === h) {
this.currentHead = h;
return i;
}
}
this.currentHead = {};
return false;
},
getRow : function(){
return this.body.querySelectorAll('tbody tr.' + this.getRowCls());
},
getRowIndex : function(t){
var i, len,
row = HL.dom.findParent(t, 'tr.' + this.getRowCls()),
tr = this.getRow();
for(i = 0, len = tr.length; i < len; i++){
if(tr[i] === row){
this.currentRow = row;
return i;
}
}
this.currentRow = {};
return false;
},
getCellIndex : function(t, r){
var i, len,
cell = HL.dom.findParent(t, 'td.' + this.getCellCls()),
tr = this.getRow(),
td = tr[r].querySelectorAll('td.' + this.getCellCls());
for(i = 0, len = td.length; i < len; i++){
if(td[i] === cell){
this.currentCell = cell;
return i;
}
}
this.currentCell = {};
return false;
},
DOM的遍历又可以单独作为一个话题来讨论,这里就说下HL.dom.findParent 方法,这是根据相应的CSS选择器来查找目标DOM上层父节点,因为grid dom结构可能会更复杂,嵌套关系复杂以后就并不一定是单纯的Tr td元素了,Ext也是采用这种方式定位源节点。
HL.dom.findParent使用了最新支持的JS原生API: querySelector,IE6不支持,关于dom遍历和CSS选择器我打算某天也开一个专门的帖子讨论,先附上源代码
findParent : function(el, selector, d){
var parent = el.parentNode,
mparent,
depth = d || 5,
nodes, node, i, len;
if(!parent){
return el;
}
mparent = parent.parentNode;
if(document.querySelector){
nodes = parent.querySelectorAll(selector);
for(i = 0, len = nodes.length; i < len; i++){
if(nodes[i] === el)
return el;
}
while(depth > 0 && mparent){
nodes = mparent.querySelectorAll(selector);
for(i = 0, len = nodes.length; i < len; i++){
if(nodes[i] === parent)
return parent;
}
parent = mparent;
mparent = parent.parentNode;
depth--;
}
return false;
} else {
return parent;
}
}
逻辑还有待优化,但功能基本没问题,下面来看看测试代码和Grid效果吧
<script type="text/javascript">
HL.load('js/base/EventMgr.js');
HL.load('js/ui/Grid.js', function(){
var cm = ['id', 'name', 'age', 'sex', 'bouns'],
ds = { data: [
['1', 'jill', '12', 'female', '20K'],
['2', 'snake', '23', 'male', '20K'],
['3', 'cash', '15', 'male', '20K'],
['4', 'rose', '28', 'female', '25K'],
['5', 'mike', '24', 'male', '20K'],
['6', 'clare', '23', 'female', '25K'],
['7', 'eve', '31', 'female', '14K']
]};
var grid = new HL.ui.Grid(
{id : 'mygrid',
renderer : 'grid',
cm : cm,
dataSource : ds});
});
</script>
<div id="grid"></div>
哈哈,是不是很想Ext的代码风格??我这里还加入了延迟加载的功能,需要某一组件时再加载它的js文件,下面看看Grid更复杂的功能如何实现的,在initEvent原型方法中加入以下代码:
this.on('cellclick', function(r, e){
if(this.selected !== this.currentCell){
if(this.selected){
HL.dom.removeClass(this.selected, this.getSelectCls());
}
this.selected = this.currentCell;
HL.dom.addClass(this.currentCell, this.getSelectCls());
}
});
this.on('rowmousemove', function(r, e){
this.clearHoverCls();
HL.dom.addClass(this.currentRow, this.getHoverCls());
});
this.on('headmousemove', function(r, e){
this.clearHoverCls();
HL.dom.addClass(this.currentHead, this.getHoverCls());
});
分别注册cellclick、rowmousemove、headmousemove 这些事件,来改变具体DOM元素的样式,实现鼠标滑过变色以及点击单元格变灰的效果。看基于观察者事件机制,增加复杂功能是不是很轻松啊!!
相关联的代码确实非常多,Grid源代码也有部分没有上传,也是怕大家看多了看烦了呵呵,有需要源代码的可以给我留言,我单独发,好了今天的UI组件之旅就到这里,最后附上Grid实际效果图: