列表組件抽象(5)-簡潔易用的表格組件


這是我寫的關於列表組件的第5篇博客。前面的相關文章有:

1. 列表組件抽象(1)-概述

2. 列表組件抽象(2)-listViewBase說明

3. 列表組件抽象(3)-分頁和排序管理說明

4. 列表組件抽象(4)-滾動列表及分頁說明

本文介紹如何實現一個簡潔易用的表格組件。

它對應的源碼是:

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

效果如下:

image

對應的演示代碼為:

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,並且大小為瀏覽器滾動條的寬度:

image

如果表頭的背景直接設置在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的寬度上即可。

到此為止的話,表格組件的一些要點也介紹完畢,關於列表組件的這一大堆文件的分享,基本上也就到此結束了,將來我自己肯定還會不斷地完善,但那更多是在項目中的實際工作了,不一定還會再寫出來,畢竟每個項目都有個性化的需求。我寫這些東西的初衷,是認為列表功能,分頁功能,排序功能之間都有相似性,為了減少重復代碼,可以做一些抽象,來讓代碼更加簡潔,更好管理。希望關於列表組件的內容能給大家帶來一些有價值的東西。謝謝這幾天的關注:)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM