xmlplus 組件設計系列之十 - 網格(DataGrid)

這一章咱們要實現是一個網格組件,該組件除了最基本的數據展現功能外,還提供排序以及數據過濾功能。css

數據源

爲了測試咱們即將編寫好網格組件,咱們採用以下格式的數據源。此數據源包含兩部分的內容,分別是表頭數據集和表體數據集。網格組件實例最終的列數由表頭數據集的長度決定。app

// 10-01
var data = { 
    gridColumns: ['name', 'power'],
    gridData: [
      { name: 'Chuck Norris', power: Infinity },
      { name: 'Bruce Lee', power: 9000 },
      { name: 'Jackie Chan', power: 7000 },
      { name: 'Jet Li', power: 8000 }
    ]
};

頂層設計

從視覺上,咱們很天然地把網格組件劃分爲表頭與表體。此網格組件有三個功能,因此應該提供三個動態接口。但咱們注意到排序功能是經過點擊表頭進行的,而表頭屬於網格組件的一部分,因此該功能應該內置。從而,實際上咱們的網格組件對外只暴露兩個動態接口:一個用於過濾,另外一個用於接收數據源。因而咱們能夠獲得以下的一個頂層設計。函數

// 10-01
DataGrid: {
    xml: `<table id='datagrid'>
            <Thead id='thead'/>
            <Tbody id='tbody'/>
          </table>`,
    fun: function (sys, items, opts) {
        function setValue(data) {
            items.thead.val(data.gridColumns);
            items.tbody.val(data.gridColumns, data.gridData);
        }
        function filter(filterKey) {
            // 過濾函數
        }
        return { val: setValue, filter: filter };
    }
}

設計表頭

表頭只有一行,因此能夠直接給它提供一個 tr 元素。tr 元素的子級項 th 元素的個數取決於表頭數據集的長度,因此須要動態建立。因爲 th 元素包含了排序功能,因此須要另行封裝。下面是咱們給出的表頭的設計。測試

// 10-01
Thead: {
    xml: `<thead id='thead'>
              <tr id='tr'/>
          </thead>`,
    fun: function (sys, items, opts) {
        return function (value) {
            sys.tr.children().call("remove");
            data.forEach(item => sys.tr.append("Th").value().val(item));
        };
    }
}

表頭數據項組件提供一個文本設置接口。該組件自己並不負責排序,它只完成自身視圖狀態的變動以及排序命令的派發。排序命令的派發須要攜帶兩個數據:一個是排序關鍵字,也就是表頭文本;另外一個排序方向:升序或者降序。this

// 10-01
Th: {
    css: "#active { color: #fff; } #active #arrow { opacity: 1; } #active #key { color: #fff; }\
          #arrow { display: inline-block; vertical-align: middle; width: 0; height: 0; margin-left: 5px; opacity: 0.66; }\
          #asc { border-left: 4px solid transparent; border-right: 4px solid transparent; border-bottom: 4px solid #fff;}\
          #dsc { border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid #fff; }",
    xml: "<th id='th'>\
            <span id='key'/><span id='arrow'/>\
          </th>",
    fun: function (sys, items, opts) {
        var order = "#asc";
        this.watch("sort", function (e, key, order) {
            sys.key.text().toLowerCase() == key || sys.th.removeClass("#active");
        });
        this.on("click", function (e) {
            sys.th.addClass("#active");
            sys.arrow.removeClass(order);
            order = order == "#asc" ? "#dsc" : "#asc";
            sys.arrow.addClass(order).notify("sort", [sys.key.text().toLowerCase(), order]);
        });
        sys.arrow.addClass("#asc");
        return sys.key.text;
    }
}

設計表體

表體能夠有多行,但表體只負責展現數據,因此實現起來比表頭要簡單的多。spa

// 10-01
Tbody: {
    xml: `<tbody id='tbody'/>`,
    fun: function (sys, items, opts) {
        return function (gridColumns, gridData) {
            sys.tbody.children().call("remove");
            gridData.forEach(data => 
                tr = sys.tbody.append("tr");
                gridColumns.forEach(key => tr.append("td").text(data[key]));
            ));
        };
    }
}

此組件提供了一個接收數據源的動態接口,數據源須要包含兩個部分:表頭數據集與表體數據集。該動態接口根據這兩個數據集完成數據的展現。設計

加入排序功能

爲了便於管理,咱們把排序功能單獨封裝成一個組件,該組件提供一個排序接口,同時偵聽一個排序消息。一旦接收到排序消息,則記錄下關鍵字與排序方向,並派發一個表體刷新命令。code

// 10-01
Sort: {
    fun: function (sys, items, opts) {
        var sortKey, sortOrder;
        this.watch("sort", function (e, key, order) {
            sortKey = key, sortOrder = order;
            this.trigger("update");
        });
        return function (data) {
            return sortKey ? data.slice().sort(function (a, b) {
                a = a[sortKey], b = b[sortKey];
                return (a === b ? 0 : a > b ? 1 : -1) * (sortOrder == "#asc" ? 1 : -1);
            }) : data;
        };
    }
}

要完整地實現排序功能,對組件 DataGrid 做一些修正,主要是內置上述的排序功能組件並偵聽表體刷新指令。一旦接收到刷新指令,則對錶體數據完成排序並刷新表體。xml

// 10-01
DataGrid: {
    xml: `<table id='table'>
            <Thead id='thead'/>
            <Tbody id='tbody'/>
            <Sort id='sort'/>
          </table>`,
    fun: function (sys, items, opts) {
        var data = {gridColumns: [], gridData: []};
        function setValue(value) {
            data = value;
            items.thead(data.gridColumns);
            items.tbody(data.gridColumns, data.gridData);
        }
        function filter(filterKey) {
            // 過濾函數
        }
        this.on("update", function() {
            items.tbody(items.sort(data.gridData));
        });
        return { val: setValue, filter: filter };
    }
}

加入過濾功能

與排序功能的加入流程相似,咱們把過濾功能單獨封裝成一個組件,該組件提供一個過濾接口,同時偵聽一個過濾消息。一旦接收到消息,則記錄下過濾關鍵字,並派發一個表體刷新命令。blog

// 10-01
Filter: {
    fun: function (sys, items, opts) {
        var filterKey = "";
        this.watch("filter", function (e, key) {
            filterKey = key.toLowerCase();
            this.trigger("update");
        });
        return function (data) {
            return data.filter(function (row) {
                return Object.keys(row).some(function (key) {
                    return String(row[key]).toLowerCase().indexOf(filterKey) > -1;
                });
            });
        };
    }
}

另外須要對組件 DataGrid 做一些修正,修正內容與上述的排序功能的加入相似,區別在於額外完善了 filter 接口以及對消息做用域進行了限制。下面是咱們最終的網格組件。

// 10-01
DataGrid: {
    css: `#table { border: 2px solid #42b983; border-radius: 3px; background-color: #fff; }
          #table th { background-color: #42b983; color: rgba(255,255,255,0.66); cursor: pointer; ... }
          #table td { background-color: #f9f9f9; }
          #table th, #table td { min-width: 120px; padding: 10px 20px; }`,
    xml: `<table id='table'>
            <Thead id='thead'/>
            <Tbody id='tbody'/>
            <Sort id='sort'/>
            <Filter id='filter'/>
          </table>`,
    map: { msgscope: true },
    fun: function (sys, items, opts) {
        var data = {gridColumns: [], gridData: []};
        function setValue(value) {
            data = value;
            items.thead(data.gridColumns);
            items.tbody(data.gridColumns, data.gridData);
        }
        function filter(filterKey) {
            sys.table.notify("filter", filterKey);
        }
        this.on("update", function() {
            items.tbody(data.gridColumns, items.filter(items.sort(data.gridData)));
        });
        return { val: setValue, filter: filter };
    }
}

值得注意的是這裏必定要在映射項中配置限制消息做用域的選項。不然,當在一個應用中實例化多個網格組件時,消息就會互相干擾。

測試

最後咱們來測試下咱們完成的組件,測試的功能主要就是剛開始提到的三個:數據展現、排序以及過濾。

// 10-01
Index: {
    css: `#index { font-family: Helvetica Neue, Arial, sans-serif; font-size: 14px; color: #444; },
          #search { margin: 8px 0; }`
    xml: `<div id='index' xmlns:i='datagrid'>
            Search <input id='search'/>
            <i:DataGrid id='datagrid'/>
          </div>`,
    fun: function (sys, items, opts) {
        items.datagrid.val({
            gridColumns: ['name', 'power'],
            gridData: [
              { name: 'Chuck Norris', power: Infinity },
              { name: 'Bruce Lee', power: 9000 },
              { name: 'Jackie Chan', power: 7000 },
              { name: 'Jet Li', power: 8000 }
            ]
        });
        sys.search.on("input", e => items.datagrid.filter(sys.search.prop("value")));
    }
}
相關文章
相關標籤/搜索