构想
表格是组件库中交互较为复杂的组件之一,需要面对的情况比较多,单纯靠css是无法写出完备的表格组件的。我们的表格组件应该具备易用易拓展能充分适应需求的特性。在前端所接触的表格中,最常见到也是最基础的就是普通的二维表格,我们可以先从这个简单的二维表格入手,一步步去完善这个组件。
开始设计
以下的Demo均以React为例,其中使用了集成在SluckyUI中的样式,虽说Vue,Angular的实现有些不同,但差别不大,思路是一样的。
首先一个表格由表头和表内容组成,其中内容的数据结构是二维的,分别是行和列。
数据结构
想象一下,表格的展示需要一种怎样的数据结构呢?没错,后端接口这时应该会返回一个像这样的数组,其中数组即作为列,数组中的对象即包含一行中所有的数据。
const data=[{
sex:'man',
age:'19'
},{
sex:'feman',
age:'20'
}]
现在确定了输入表格组件的数据结构。接下来就是灵魂三问,表格怎样才能知道对应的key渲染到哪一列,怎样才知道列的长度,怎样才知道列的标题呢?嗯,这个时候我们的表格组件就应该要有一个选项可针对每一列进行配置。
ok,就像这样对每一列进行相应的配置
dataConf=[{
title:'自定义列的名称(别名)',
name:'匹配data里对应的字段如age',
width:'200px||20%'
},{
title:'年龄',
name:'age',
width:'20%'
}]
那么关键的地方来了,现在我们有了配置和数据,需要做的就是根据所写的配置对输入表格的数据进行相应的渲染。
表格构造
表格头部
即列的标题,这一部分是与表的内容分开的,所以我们单独去处理它
...
<div className="table-head">
{
this.props.dataconf.map((conf, i) => {
return <div style={{ 'width': conf.width }} key={i}>{conf.title||''}</div>
})
}
</div>
...
这个处理很简单,一个循环就搞定了
表格内容
渲染一个二维的数据结构也不难,两个普通的for
循环完成了。
// table.jsx
...
<div className="table-body">
{
dataset.map((data, i)=>{
return (
<div className="table-row">
{
dataconf.map((conf, k) => {
return <div className="table-data">data[conf.name]</div>
})
}
</div>
)
})
}
</div>
...
就这样,一个表格组件的基本部分就搭建起来了。
布局考虑
曾经想过直接用<table></table>
系列直接去解决表格的布局问题,这样做的确方便快捷,改动又小。但后来随着需求逐渐变得复杂时,发现单单靠<table></table>
系列会有很多问题无法解决,面对复杂需求时局限性比较大。
在对比几种布局方式之后,决定使用dev-flex
布局,原因很简单,dev-flex
布局简洁而强大,能够很好地处理各种复杂的需求。
//table.css
//表格中一行的样式
.table-row{
display: flex;
justify-content: space-between;
align-items: center;
}
Note:用div-flex这种布局很灵活,对不同场景的适应性很强,但是有一个小缺点,这是后来才知道的,就是鼠标去框选复制渲染出来的表格内容后,再粘贴到excel中就会出现格式混乱,而用传统table布局就能显示出完整表格,也不知道excel是什么时候支持表格识别的。。。
功能拓展
很多时候我们需要的表格不单单只是去展示数据,还有对列进行排序,对行进行增删减。关键点又来了,我们究竟怎样才能方便地将对应行的数据传到需要用到的地方呢?听起来可能很绕,就拿删除某行数据来讲,我们需要做的就是获取对应行的id,然后发起网络请求,删除id对应的行。嗯,思路很清晰。如果用React去实现的话,应该怎样做呢?
// table.jsx
...
<div class="table-body">
{
dataset.map((data, i)=>{
return (
<div className="table-row">
{
dataconf.map((conf, k) => {
return (
<div style={{ 'width': conf.width }}>
{
!conf.rander?<div className="table-data">data[conf.name]</div>:null
}
{
conf.rander?<div className="table-data">{conf.rander(data, i)}</div>:null
}
</div>
)
})
}
</div>
)
})
}
</div>
...
没错,在恰当的地方很巧妙地设置了一个函数回调,用来传出对应行的数据,然后我们这样去进行配置。
dataConfig=[{
width: '10%',
rander:(data,index)=>{
return <div onClick={()=>{
conslog(data)
http.delRecord(data.id)
}}>删除</div>
}
}]
如果用Angular2+去实现类似的Api的话,最关键就是要解决作用域传递的问题,没有jsx这么灵活,这里就不做过多分析了。
一些有趣的功能
当然,我们的表格有了rander选项之后,就已经将用户操作完全解耦出来。这种情况下再为表格组件集成用户操作相关的功能只是为了方便调用。
进度条
// table.jsx
...
<div class="table-body">
{
dataset.map((data, i)=>{
return (
<div className="table-row">
{
dataconf.map((conf, k) => {
return (
<div style={{ 'width': conf.width }}>
...
<div className="d-il">
{
!conf.pipe ? (
<span className="p-r z10">{data[conf.name]}</span>
) : null
}
<progress max="100" value={conf.progress && conf.progress(data)}
className="progress-loading"></progress>
</div>
...
</div>
)
})
}
</div>
)
})
}
</div>
...
dataConfig=[{
width: '10%',
title: 'progress',
width: '20%',
progress: () => {
//返回0-100表示进度百分比
return 50
},
}]
气泡提示
// table.jsx
...
<div class="table-body">
{
dataset.map((data, i)=>{
return (
<div className="table-row">
{
dataconf.map((conf, k) => {
return (
<div style={{ 'width': conf.width }}>
...
<div class="pop-box">
<div className="pop-toggle ptb4 mlr4">
<div className="pop-main pr8">
<div className="pop-content">
{conf.popup(data, i)}
</div>
</div>
</div>
</div>
...
</div>
)
})
}
</div>
)
})
}
</div>
...
dataConfig=[{
width: '10%',
title: 'popup',
width: '20%',
popup: () => {
return <button>气泡提示</button>
},
}]
完整版请看这里完整版Table组件&&所用到的样式,觉得不错的话不妨点个star哈哈。
注:样式又是另一个话题了,可参看《Re从零开始的UI库编写生活之规范制定》
结尾
其实表格组件并不像想象中那么难,只要理清楚思路,把该解耦的部分抽离出来,剩下的工作就是发挥想象力,往里面添加各种功能而已。更多有趣的组件尽在SluckyUI中,欢迎多多交流,期待你的加入。
从零开始系列传送门
- 《Re从零开始的UI库编写生活之规范制定》
- 《Re从零开始的UI库编写生活之按钮》
- 《Re从零开始的UI库编写生活之表单》
- 《Re从零开始的UI库编写生活之表格》
- 《Re从零开始的UI库编写生活之加载进度条》
- 《Re从零开始的UI库编写生活之分页》
- 《Re从零开始的UI库编写生活之菜单导航栏》
- 《Re从零开始的UI库编写生活之消息弹窗》
- 《Re从零开始的UI库编写生活之步骤管理器》
- 《Re从零开始的UI库编写生活之面包屑》
- 《Re从零开始的webpack4实用向全实践》
- 《Re从零开始的高效React+Redux项目架构搭建》
- Re从零开始的后端学习之配置Ubuntu+Ngnix+Nodejs+Mysql环境