這是我寫的關於列表組件的第5篇博客。前面的相關文章有:css
1. 列表組件抽象(1)-概述html
2. 列表組件抽象(2)-listViewBase說明jquery
3. 列表組件抽象(3)-分頁和排序管理說明git
4. 列表組件抽象(4)-滾動列表及分頁說明github
本文介紹如何實現一個簡潔易用的表格組件。ajax
它對應的源碼是:json
https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/tableView.jsapi
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的寬度上便可。
到此爲止的話,表格組件的一些要點也介紹完畢,關於列表組件的這一大堆文件的分享,基本上也就到此結束了,未來我本身確定還會不斷地完善,但那更可能是在項目中的實際工做了,不必定還會再寫出來,畢竟每一個項目都有個性化的需求。我寫這些東西的初衷,是認爲列表功能,分頁功能,排序功能之間都有類似性,爲了減小重複代碼,能夠作一些抽象,來讓代碼更加簡潔,更好管理。但願關於列表組件的內容能給你們帶來一些有價值的東西。謝謝這幾天的關注:)