组件是一个可复用的类,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实际效果图: