從零開始使用JavaScript編寫資料表格控制元件(二)

民工精髓發表於2013-10-31

資料表格控制元件的渲染器

在第一部分中,我們講述瞭如何實現一個最簡單的資料表格控制元件,在後面的部分,我們會討論得深入一些,探討資料表格的渲染器、排序等功能。

5. 渲染器

什麼叫做渲染器呢?

之前我們的DataGrid非常簡單,每個單元格只是簡單的把文字資料渲染出來,如果想要對這些文字作格式化處理,比如說,把性別的標記0和1轉換成文字的“男”和“女”,要怎麼去考慮?

有個笨辦法,我們讓使用者傳入資料之前,就把原始資料修改一下,比如說,原先資料格式是這樣:

{
    name: "Tom",
    age: 16,
    gender: 1
}

我們給他先轉換一遍,變成:

{
    name: "Tom",
    age: 16,
    gender: 1,
    genderName: "Male"
}

這樣,不顯示gender這個列,而是直接顯示genderName這一列,也可以做到想要的效果。這麼看上去簡單方便,有沒有什麼弊端呢?有三種。

  • 破壞了原始資料
  • 需要對資料作預處理,這個處理過程比較集中,我們在前端常見的優化策略是把計算儘可能均攤,避免某個短時間的密集計算。
  • 把邏輯割裂了。為什麼這麼說呢,因為你不但要在批量載入資料之前做這個轉換,新增、修改行的時候,是不是也同時要?

另外也有個辦法,可以預定義一些規則,比如說定義了這個是性別的列,把格式化的過程內建在控制元件中,這種做法也不好,內建的東西總是有限的,面對不斷變更的需求,需要無休止地修改。

現在討論的只是單個列需要處理,假如有多個,那更麻煩了,有沒有什麼更好的辦法呢?

5.1 欄位格式化

我們可以把這個格式化功能提取到外面,然後注入進來。格式化的功能,至少應當是針對列的,所以可以附加到列的初始化資訊裡傳遞過來。考慮一下格式化函式的引數,至少需要原始值,但有些情況更加複雜,所以我們多給它一些資訊,比如說,本行的完整資料,還有當前列的key值。這麼一來,一個典型的格式化函式就有了:

function labelFunction(data, key) {
    var value = data[key];
    if (value == 0) {
        return "Female";
    }
    else if (value == 1) {
        return "Male";
    }
    else {
        return "Unknown"
    }
}

有了格式化,我們就可以很方便地進行一些顯示的轉換,比如對日需求,期、金額的實際值和顯示值進行轉換,或者,也可以顯示一些圖片和操作按鈕之類。

比如說:

function labelFunction(data, key) {
    var value = data[key];
    if (value > 18) {
        return "<button>Click me, man</button>";
    }
    else {
        return "Hi, boy, you can do nothing.";
    }    
}

上面這段程式碼是一個示例,我們可以指定當年齡大於18歲的時候出來一個按鈕可點,不足18的時候只出來一段文字。看上去,這段程式碼也滿足我們需要了,但它將會遇到問題。

什麼問題呢?我們來給這個按鈕加個事件,點選它的時候,顯示這個人的名字。考慮到我們輸出的是HTML字串,所以這個事件比較難加,除非也用字串拼到裡面,這麼做是有很多弊端的,我們來考慮用一些優雅的方式解決。

5.2 單元格渲染器

既然返回字串不好,那我們直接一點,返回DOM結構如何?

var itemRenderer = {
    render: function (row, key, columnIndex) {
        var data = row.data;

        if (data[key] >= 18) {
            var btn = document.createElement("button");
            btn.innerHTML = data[key];
            btn.onclick = function () {
                alert("I am " + data[key] + " years old, I want a bottle of wine!");
            };

            return btn;
        }
        else {
            var span = document.createElement("span");
            span.innerHTML = data[key];
            return span;
        }
    },
    destroy: function () {

    }
};

這樣就好多了。這個時候,我們需要考慮渲染器和格式化函式的優先順序,有人會問,有了渲染器,還要格式化函式幹什麼?這問題其實就像有了拖拉機,為什麼鋤頭還能賣得出去?我們把渲染器當作一個比較重量級的解決方案,格式化函式當作輕量級的,各有其使用場景。

我們來看看行的渲染方法應當怎麼寫:

render: function (cell, data, field, index) {
    if (this.grid.columns[index].itemRenderer) {
        cell.innerHTML = "";
        cell.appendChild(this.grid.columns[index].itemRenderer.render(this, field, index));
    }
    else if (this.grid.columns[index].labelFunction) {
        cell.innerHTML = "";
        cell.innerHTML = this.grid.columns[index].labelFunction(data, field);
    }
    else if (this.grid.itemRenderer) {
        cell.innerHTML = "";
        cell.appendChild(this.grid.itemRenderer.render(this, field, index));
    }
    else {
        cell.innerHTML = data[field];
    }
}

這裡面有四種東西:

  • 針對某列的渲染器
  • 針對某列的格式化函式
  • 針對所有單元格的全域性渲染器
  • 直接賦值

我們讓它們的優先順序遞減。為什麼會同時需要全域性渲染器和列的渲染器呢?其實也可以在全域性渲染器裡面對行、列作判斷,然後分別為每種情況渲染,但如果很多列都需要渲染,這麼做不太好,需要分離成多個不同的列渲染器。

注意到我們使用的渲染器裡面帶有destroy方法,這個是為了減少記憶體洩露而設計的,使用者可以自行在這裡解除安裝事件處理函式,隔斷待回收的物件引用。

現在我們實現了單元格的渲染器機制,那麼,標題的列頭呢?這裡可能也會需要有定製的內容,所以也需要為它設計類似的擴充套件機制,在此不再贅述。

5.3 資料表格的複選功能

有了這些渲染器機制,我們可以來為資料表格新增更實用的功能。很多資料表格的使用場景需要複選,標題上有一個核取方塊,可以控制行的選中狀態,行的選中狀態也會反過來影響到標題核取方塊的選中狀態。

所以,我們需要兩個渲染器,一個是放在標題上的,一個是放在行上的。

var CheckboxRenderer = {
    render: function(row, field, columnIndex) {
        var grid = row.grid;
        var data = row.data;

        var div = document.createElement("div");
        var checkbox = document.createElement("input");
        checkbox.type = "checkbox";
        checkbox.checked = data["checked"];

        checkbox.onclick = function () {
            data["checked"] = !data["checked"];

            var checkedItems = 0;
            var rowLength = grid.rows.length;
            for (var i=0; i<rowLength; i++) {
                if (grid.rows[i].get("checked")) {
                    checkedItems++;
                }
            }

            if (checkedItems === 0) {
                grid.set("checkState", "unchecked");
            }
            else if (checkedItems === rowLength) {
                grid.set("checkState", "checked");
            }
            else {
                grid.set("checkState", "indeterminate");
            }
        };
        div.appendChild(checkbox);

        var span = document.createElement("span");
        span.innerHTML = data[field];
        div.appendChild(span);

        return div;
    }
};

var HeaderRenderer = {
    render: function (grid, field, columnIndex) {
        var rows = grid.rows;
        var div = document.createElement("div");
        var checkbox = document.createElement("input");
        checkbox.type = "checkbox";

        switch (grid.get("checkState")) {
            case "checked": {
                checkbox.checked = true;
                break;
            }
            case "unchecked": {
                checkbox.checked = false;
                break;
            }
            case "indeterminate": {
                checkbox.indeterminate = true;
                break;
            }
        }
        div.appendChild(checkbox);

        checkbox.onclick = function () {
            var checked = this.checked;
            for (var i = 0; i < rows.length; i++) {
                rows[i].set("checked", checked);
            }
        };

        var span = document.createElement("span");
        span.innerHTML = field;
        div.appendChild(span);

        return div;
    },

    destroy: function() {

    }
};

之前,我們並不能完全確定應當傳遞給渲染器哪些引數,可能會認為只需要當前資料和列的key值即可,在做這個例子的過程中,會發現可能還需要datagrid本身的例項,所以,直接把行的例項傳入即可,從它身上可以直接獲得datagrid的例項和行的資料。除此之外,列序號也可以傳遞進來,雖然說它跟key可以互相反查得到,但直接傳入會比較便利些。

5.4 小結

到目前為止,我們的資料表格控制元件裡可以展示一些複雜的東西了,比如說一些操作按鈕,圖片,甚至繪製一些圖形,更重要的是,它們可以跟控制元件本身產生互動,而又不需要修改控制元件自身的程式碼。

所以說,這個DataGrid控制元件還是比較靈活的,可以支援有一定複雜度的需求了,但是還是有缺陷。這種機制,如果想要渲染出跨行或者跨列的表格,就有一些難度了,我們不在這個方面多作文章,只針對90%的需求編寫程式碼。

本節提到的程式碼,示例在:

http://xufei.github.io/thin/demo/controls/datagrid.html

請讀者自行檢視。

相關文章