微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

列表组件抽象(5):简洁易用的表格组件

2016-10-02 20:02 前端大全

(点击上方公众号,可快速关注)


作者:流云诸葛

链接:www.cnblogs.com/lyzg/p/5881357.html



本文介绍如何实现一个简洁易用的表格组件。


它对应的源码是:


https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/tableView.js


https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/tableDrag.js


https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/tableOrder.js


https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/tableDefault.js


其中tableView是表格组件的核心。tableDrag和tableOrder是我写的两个插件,分别让表格支持列宽调整和自动生成序号列。tableDefault目前的作用仅仅是简化插件的配置。下面的demo可以让你了解下它的基本功能:


http://liuyunzhuge.github.io/blog/form/dist/html/tableView.html


效果如下:



对应的演示代码为:


http://liuyunzhuge.github.io/blog/form/dist/js/app/tableView.js


在了解它的详细实现思路前,你可以通过上面的演示代码来查看这个组件的使用方式。整体上它与其它列表组件的用法类似,但是由于表格组件在结构和功能上的特异性,所以它在实例化的时候要用到好几个其它列表组件不具备的option。实例代码如下:


define(function (require) {

 

    var $ = require('jquery'),

        ListView = require('mod/listView/tableView'),

        TableDefault = require('mod/listView/tableDefault'),

        api = {

            list: './api/tableView.json',

        };

 

    var list = new ListView('#table_view', {

        multipleSelect: true,

        heightFixed: true,

        url: api.list,

        tableHd: ['<tr>',

            '    <th>序号</th>',

            '    <th><input type="checkbox" class="table_check_all"></th>',

            '    <th data-field="name" data-drag="false" class="sort_item">姓名 <i class="sort_icon"></i></th>',

            '    <th data-field="contact" data-drag-min="100" data-drag-max="200" class="sort_item">联系方式 <i class="sort_icon"></i></th>',

            '    <th data-field="email" class="sort_item">邮箱 <i class="sort_icon"></i></th>',

            '    <th>昵称</th>',

            '    <th>备注</th>',

            '</tr>'].join(""),

        colgroup: ['<colgroup>',

            '    <col width="70">',

            '    <col width="40">',

            '    <col width="120">',

            '    <col width="120">',

            '    <col width="180">',

            '    <col width="180">',

            '    <col  width="200">',

            '</colgroup>'].join(""),

        tpl: ['{{#rows}}<tr>',

            '<td><span class="table_view_order"></span></td>',

            '<td align="middle" class="tc"><input type="checkbox" class="table_check_row"></td>',

            '<td>{{name}}</td>',

            '<td>{{contact}}</td>',

            '<td>{{email}}</td>',

            '<td>{{nickname}}</td>',

            '<td><button class="btn-action" type="button">操作</button></td>',

            '</tr>{{/rows}}'].join(''),

        sortView: {

            config: [

                {field: 'name', value: ''},

                {field: 'contact', value: 'desc', order: 2},

                {field: 'email', value: 'asc', order: 1}

            ]

        },

        pageView: {

            defaultSize: 20

        },

        plugins: TableDefault.plugins

    });

 

    list.$element.on('click','.btn-action', function(e) {

        console.log(list.getRowData($(this).closest('tr').index()));

    });

 

    list.query();

});


补充:在demo中,我用heightFixed这个option用来表示是否要固定表格的高度;用plugins这个option来配置当前实例要扩展的插件功能,利用到了tableDefaults来注册默认的插件组;最后通过jquery的形式给表格行内的某个dom元素绑定了点击事件,并在回调内通过表格组件的实例化方法getRowData获取到了该行对应的表格数据。


这个表格组件除了支持分页查询,排序之外,还支持以下功能:


1. 序号列生成,列宽调整,理论上可以通过自定义插件的方式再扩展更多的功能;比如树形表格、表格编辑等;可直接通过表格实例,对插件进行增删改查;


2. 自由切换是否固定表格高度,当表格高度固定时,表头会固定,然后表体会以auto模式控制滚动条;滚动时,表头由于固定所以不会被遮挡,便于用户查看表格数据;如果表格高度不固定,那么表体就不会出现滚动条,表头固定也就没有意义了;


3. css完全灵活,可通过option改变表格组件内部实现时需要的所有css class;


4. html结构相对灵活,表头和表体的html都以模板的形式定义,如需在表头和表体中插入相对个性化的内容,直接在模板中插入即可;


5. 支持表格行,单选和多选;当然两种模式只能用其一;


6. 可方便地根据表格行的索引,获取该行对应的原始数据和解析后的数据;原始数据就是ajax返回后未经parseData这个option处理的数据;解析后的数据就是parseData处理后的数据;


7. 可方便地获取所有选中行的单个属性值;


8. 当窗口resize,DOM更改等影响到表格内容的时候,表格的布局会自动调整;也支持手动触发调整;


总的来说,这个组件的实现并不麻烦,就是因为要实现的功能多,所以内容也多。


首先,先来了解下它html结构:


<div id="table_view" class="table_view table_view_init">

    <div class="table_view_hd">

        <table class="table_hd">

        </table>

    </div>

    <div class="table_view_bd">

        <table class="table_bd">

        </table>

    </div>

    <div class="table_ft_view">

        <ul class="table_page_view">

        </ul>

    </div>

</div>


它把表格组件分成表头、表体、表尾三部分。表头显示表格标题行;表体显示数据;表尾显示分页组件。因为要考虑做表头固定,所以标题行和数据行不能属于同一个table元素。固定只能利用绝对定位来做。


在设置css的时候,边框和表头的背景色的设置比较关键:


1. 不管是表头的table和表体的table,都没有上下左右边框,你看到的边框都是由table的包裹元素设置的。这么做的目的,是为了表格组件在UI上的整体性考虑的。


2. 表头的背景不是设置在表头的table上,而是设置在table的包裹元素上。这么做的原因还是跟UI整体性有关,当表体出现滚动条时,表体内的table的宽度变窄,为了让表头内的table宽度与表体内table的宽度保持一致,必须给表头添加一个padding-right,并且大小为浏览器滚动条的宽度:



如果表头的背景直接设置在table上,那么padding-right那个位置,将让人认为是页面上多余的空间,看起来别扭。


接下来看看表格组件源码中的要点。


1)defaults如下:


var DEFAULTS = $.extend({}, ListViewBase.DEFAULTS, {

    //是否固定高度,如果固定高度,将会在合适的时候添加纵向滚动条

    heightFixed: false,

    //colgroup的html

    colgroup: '',

    //用来作为标题行的html

    tableHd: '',

    tableViewInitClass: 'table_view_init',

    tableViewHdClass: 'table_view_hd',

    tableHdClass: 'table_hd',

    tableViewBdClass: 'table_view_bd',

    tableBdClass: 'table_bd',

    tableFtViewClass: 'table_ft_view',

    dataListClass: 'data_list',

    pageViewClass: 'table_page_view',

    //布局改变时的回调

    adjustLayout: $.noop,

    //是否进行多列选择

    multipleSelect: false,

    //行选中时添加的css类

    selectedClass: 'selected',

    //全选的checkbox的l类名

    allCheckboxClass: 'table_check_all',

    //单选的checkbox类名

    rowCheckboxClass: 'table_check_row',

    //插件列表

    plugins: [],//{plugin: TableDrag, options: {...}}

});


需要说明的有:


allCheckboxClass和rowCheckboxClass对应全选和单选的checkbox,由于checkbox并不是在组件内部写死的,而是定义在模板内,所以必须通过选择器才能使用。这个比直接把checkbox写在组件内部的好处是,大大增加增加组件的灵活性;


plugins在配置的时候,用字面量对象来传递单个插件的定义。如{name: “tableDrag”,plugin: TableDrag, options: {…}},name用来标识插件实例,方便管理插件;plugin表示插件的构造函数,options传递插件需要的option。


2)表格组件的初始化方法都比较简单,主要是根据tableHd,colgroup这些option初始化表格组件的DOM结构;初始化布局;初始化行选择的功能;初始化插件实例。


3)setRowSelected是一个实例方法,接受表格行的jq对象,并将其设置为选中状态。外部也可直接调用它来实现手工选中行。


4)setUpTableSelect是一个内部用的实例方法, 初始化行选择的功能。


5)getSelectedTrs是一个实例方法,返回选中行的jq对象。外部可使用。


6)getSelectedIndexs是一个实例方法,返回选中行的索引,数组形式。外部可使用。


7)getRowData是一个实例方法,传入一个索引,返回该索引对应行的经过parseData解析后的数据。


8)getOriginalRowData是一个实例方法,传入一个索引,返回该索引对应行的原始数据。


9)getFields是一个实例方法,传入一个属性名称,返回所有选中行的解析后的数据中该属性的值。


10)getOriginalFields是一个实例方法,传入一个属性名称,返回所有选中行的原始数据中该属性的值。


11)getPlugin是一个实例方法,传入插件定义时的name值,即可返回该插件的实例,


12)addPlugin是一个实例方法,用来手工实例化一个插件,它接收一个满足plugins option元素要求的对象,用来实例化插件。


addPlugin: function (config) {

    if (!config.name) {

        throw "plugin config must have [name] option";

    }

    if (!config.plugin) {

        throw "plugin config must have [plugin] option";

    }

    if (!isFunc(config.plugin)) {

        throw "plugin config 's [plugin] options must be a constructor";

    }

 

    this.removePlugin(name);

    this.plugins[config.name] = new config.plugin(this, config.options);

},


每个插件实例化的时候,都会给它的构造函数,传入两个参数,一个是表格组件本身,另外一个就是插件相关的options。意味着所有的插件的构造函数都得按这个形式来。


13)removePlugin是一个实例方法,用来销毁某个插件的实例。销毁除了要考虑功能的取消逻辑,还要考虑好内存泄漏的问题,所以一定要检查插件所有的可能会导致内存泄漏的地方,尤其是那些绑定的事件。这个方法内部会通过调用插件的destroy方法来完成销毁,所以在定义插件的时候最好是提供这样一个方法:


removePlugin: function (name, args) {

    var plugin = this.getPlugin(name);

    if (!plugin) return;

 

    //插件必须定义destroy方法,才能有效的回收内存

    if (isFunc(plugin.destroy)) {

        plugin.destroy.apply(plugin, args);

    }

 

    delete this.plugins[name];

},


14)adjustLayout是一个实例方法。初始化完毕,浏览器窗口调整,以及查询完毕之后都会主动调用,以更新table的UI布局。外部也可直接调用,尤其是在外部更改table的DOM内容,而table不知道的情况下,以防UI错乱。


adjustLayout: function () {

    this.adjustPaddingTop();

    this.adjustTableHdViewPos();

    this.adjustTableBdViewHeight();

    this.checkTableBdScrollState();

 

    this.trigger('adjustLayout' + this.namespace);

},


从代码可看出,它主要做的有以下几件事情:


adjustPaddingTop: 由于表头固定,采用绝对定位,所以表格组件整体得设置padding-top,这个值在DOM变化的时候就要更新,防止表头盖住表体;


adjustTableHdViewPos: 由于表头固定,当表体横向滚动时,靠它来更新表头的位置,以便表头的每一列都能跟表体的每一列对齐;


adjustTableBdViewHeight: 跟第一个同理,表头高度变化后,在表格高度固定时,表体高度也会变化,所以要重新设置表体的高度,以便浏览器更新overflow的状态;


checkTableBdScrollState: 检查表体是否出现横向滚动,如果是,则给表头添加宽度等于滚动条宽度的padding-right,否则就去掉。


以上就是表格组件的核心要点了。单个点相关的代码逻辑都不是特别复杂,所以大部分都没有特意给出相应源码说明。


再来说说默认插件之一:tableOrder的定义。


它只有一个option:


var DEFAULTS = {

    orderTextClass: 'table_view_order'

};


作用完全类似于checkbox那两个option,插件利用它找到合适的位置显示序号。


这个类很简单:


define(function (require, exports, module) {

    var $ = require('jquery'),

        Class = require('mod/class');

 

var DEFAULTS = {

    orderTextClass: 'table_view_order'

};

 

    function class2Selector(classStr) {

        return ('.' + $.trim(classStr)).replace(/\s+/g, '.');

    }

 

    //给tableView添加序号列的功能

    var TableOrder = Class({

        instanceMembers: {

            init: function (tableView, options) {

                var opts= $.extend({}, DEFAULTS, options),

                    $tableBd = tableView.$tableBd,

                    pageView = tableView.pageView;

 

                if(!pageView) return;

 

                this.tableView = tableView;

                this.onSuccess = function(){

                    var start = pageView.data.start;

                    $tableBd.find('>tbody>tr>td ' + class2Selector(opts.orderTextClass)).each(function(i,e){

                        $(this).text(start + i);

                    });

                };

 

                tableView.$element.on('success.' + tableView.namespace, this.onSuccess);

            },

            destroy: function(){

                this.tableView.$element.off('success.' + this.tableView.namespace, this.onSuccess);

                this.onSuccess = undefined;

            }

        }

    });

 

    return TableOrder;

});


只要根据pageView实例,拿到它的data属性就能使用其中的start,end来生成序号,start,end分别表示当前请求的记录范围的起止索引。它提供了destroy方法,以防有需要销毁的场景。然后它的构造函数也是含之前介绍表格组件的addPlugin方法时的说明来的,第一个参数表格组件的实例,第二个参数options。


最后再来说默认插件之一:tableDrag的定义。


这个相对逻辑多一点。我这里也只介绍我的思路。


1)生成拖拽的“把手”。我这里是用一个空的元素,通过绝对定位的方式,显示在每个单元格的右边框上来处理的。当鼠标移上去变成可拖拽的模式时,点击鼠标拖动,就能调整列宽。


createDraggers: function () {

    var $tableHd = this.tableView.$tableHd;

    var opts = this.options;

 

    $tableHd.find('>thead>tr>th,>thead>tr>td').each(function () {

        var $td = $(this);

        //配置了data-drag="false"的列不能进行排序操作

        if ($td.data('drag') !== false) {

            $td.append('<span class="' + opts.draggerClass + '"></span>');

        }

    });

},


2)列在拖拽过程中,宽度的变化,我都是colgroup中对应的col元素来实现的。而不是直接通过控制td的宽度。这种方式也更符合标准。


3)这个组件里面有一处比较关键的代码:


var that = this;

this.onAdjustLayout = function () {

    var tdWidthMap = {}, total = 0, $tableHeadTds = tableView.$tableHd.find('>thead>tr>th,>thead>tr>td');

    $tableHeadTds.each(function (i, td) {

        var curWidth = $(td).outerWidth();

 

        if (i == ($tableHeadTds.length - 1)) {

            curWidth = tableView.$tableHd.outerWidth() - total;

        } else {

            total += curWidth;

        }

        tdWidthMap[i] = curWidth;

    });

 

    that.$tableHdColgroup.children('col').each(function (i, col) {

        $(col).attr('width', tdWidthMap[i]);

    });

    that.$tableBdColgroup.children('col').each(function (i, col) {

        $(col).attr('width', tdWidthMap[i]);

    });

};

 

//在tableView触发adjustLayout事件的时候,必须重新计算所有col的宽度,保证拖拽的效果

tableView.on('adjustLayout' + tableView.namespace, this.onAdjustLayout);


它的作用是监听表格组件的adjustLayout事件,在事件回调内更新所有col的宽度。之所以这么干,是因为表格的列宽是会自动调整的,尤其在表格布局改变之后,列宽的实际宽度不一定等于col上定义的宽度,所以要在表格布局改变的时候重新计算各个col的宽度,下次拖拽的结果才能正确。


具体拖拽的实现逻辑就跟平常做的那些拖拽没区别了:在鼠标点下的时候记好位置,拖动过程中,用最新的鼠标位置与最初的位置,就能得到拖拽实时的偏移距离。然后再计算到col的宽度上即可。


到此为止的话,表格组件的一些要点也介绍完毕,关于列表组件的这一大堆文件的分享,基本上也就到此结束了,将来我自己肯定还会不断地完善,但那更多是在项目中的实际工作了,不一定还会再写出来,毕竟每个项目都有个性化的需求。我写这些东西的初衷,是认为列表功能,分页功能,排序功能之间都有相似性,为了减少重复代码,可以做一些抽象,来让代码更加简洁,更好管理。希望关于列表组件的内容能给大家带来一些有价值的东西。谢谢这几天的关注:)



关注「前端大全」

看更多精选前端技术文章

↓↓↓


 
前端大全 更多文章 纯 HTML+CSS+JS 编写的计算器应用 我在学习编程中犯的两个最大错误 微信小程序开发教程(第4弹) 微信小程序开发教程(第3弹) 微信小程序开发教程(第2弹)
猜您喜欢 云计算的第二个十年 -- IT 行业的新变化 斯坦福史上最颠覆思维的教程:​你如何在两小时里用1块钱赚100块? 【陆勤笔记】《深入浅出统计学》1信息图形化:第一印象 帮你找到北!精讲UI自动化测试定位技术-清晨每日分享-吴老电台 一文看透丑陋而又神奇的JSX