Re從零開始的UI庫編寫生活之表格元件

FlyTeng_1874發表於2019-07-15

構想

表格是元件庫中互動較為複雜的元件之一,需要面對的情況比較多,單純靠css是無法寫出完備的表格元件的。我們的表格元件應該具備易用易擴充能充分適應需求的特性。在前端所接觸的表格中,最常見到也是最基礎的就是普通的二維表格,我們可以先從這個簡單的二維表格入手,一步步去完善這個元件。

開始設計

以下的Demo均以React為例,其中使用了整合在SluckyUI中的樣式,雖說Vue,Angular的實現有些不同,但差別不大,思路是一樣的。

首先一個表格由表頭和表內容組成,其中內容的資料結構是二維的,分別是行和列。

資料結構

想象一下,表格的展示需要一種怎樣的資料結構呢?沒錯,後端介面這時應該會返回一個像這樣的陣列,其中陣列即作為列,陣列中的物件即包含一行中所有的資料。

const data=[{
    sex:'man',
    age:'19'
},{
    sex:'feman',
    age:'20'
}]
複製程式碼

現在確定了輸入表格元件的資料結構。接下來就是靈魂三問,表格怎樣才能知道對應的key渲染到哪一列,怎樣才知道列的長度,怎樣才知道列的標題呢?嗯,這個時候我們的表格元件就應該要有一個選項可針對每一列進行配置。

ok,就像這樣對每一列進行相應的配置

dataConf=[{
    title:'自定義列的名稱(別名)',
    name:'匹配data裡對應的欄位如age',
    width:'200px||20%'
},{
    title:'年齡',
    name:'age',
    width:'20%'
}]
複製程式碼

那麼關鍵的地方來了,現在我們有了配置和資料,需要做的就是根據所寫的配置對輸入表格的資料進行相應的渲染。

表格構造

表格頭部

即列的標題,這一部分是與表的內容分開的,所以我們單獨去處理它

...
<div className="table-head">
    {
        this.props.dataconf.map((conf, i) => {
            return <div style={{ 'width': conf.width }} key={i}>{conf.title||''}</div>
        })
    }
</div>
...

複製程式碼

這個處理很簡單,一個迴圈就搞定了

表格內容

渲染一個二維的資料結構也不難,兩個普通的for迴圈完成了。

// table.jsx
...
<div className="table-body">
    {
        dataset.map((data, i)=>{
            return (
                <div className="table-row">
                    {
                        dataconf.map((conf, k) => {
                            return <div className="table-data">data[conf.name]</div>
                        })
                    }
                </div>
            )
        })
    }
</div>
...
複製程式碼

就這樣,一個表格元件的基本部分就搭建起來了。

image

佈局考慮

曾經想過直接用<table></table>系列直接去解決表格的佈局問題,這樣做的確方便快捷,改動又小。但後來隨著需求逐漸變得複雜時,發現單單靠<table></table>系列會有很多問題無法解決,面對複雜需求時侷限性比較大。 在對比幾種佈局方式之後,決定使用dev-flex佈局,原因很簡單,dev-flex佈局簡潔而強大,能夠很好地處理各種複雜的需求。

//table.css
//表格中一行的樣式
.table-row{
    display: flex;
    justify-content: space-between;
    align-items: center;
}
複製程式碼

Note:用div-flex這種佈局很靈活,對不同場景的適應性很強,但是有一個小缺點,這是後來才知道的,就是滑鼠去框選複製渲染出來的表格內容後,再貼上到excel中就會出現格式混亂,而用傳統table佈局就能顯示出完整表格,也不知道excel是什麼時候支援表格識別的。。。

功能擴充

很多時候我們需要的表格不單單只是去展示資料,還有對列進行排序,對行進行增刪減。關鍵點又來了,我們究竟怎樣才能方便地將對應行的資料傳到需要用到的地方呢?聽起來可能很繞,就拿刪除某行資料來講,我們需要做的就是獲取對應行的id,然後發起網路請求,刪除id對應的行。嗯,思路很清晰。如果用React去實現的話,應該怎樣做呢?

image

// table.jsx
...
<div class="table-body">
    {
        dataset.map((data, i)=>{
            return (
                <div className="table-row">
                    {
                        dataconf.map((conf, k) => {
                            return (
                                <div style={{ 'width': conf.width }}>
                                   {
                                        !conf.rander?<div className="table-data">data[conf.name]</div>:null
                                   }
                                   {
                                        conf.rander?<div className="table-data">{conf.rander(data, i)}</div>:null
                                   }
                                </div>
                            )
                        })
                    }
                </div>
            )
        })
    }
</div>
...
複製程式碼

沒錯,在恰當的地方很巧妙地設定了一個函式回撥,用來傳出對應行的資料,然後我們這樣去進行配置。

dataConfig=[{
    width: '10%',
    rander:(data,index)=>{
        return <div onClick={()=>{
            conslog(data)
            http.delRecord(data.id)
        }}>刪除</div>
    }
}]
複製程式碼

如果用Angular2+去實現類似的Api的話,最關鍵就是要解決作用域傳遞的問題,沒有jsx這麼靈活,這裡就不做過多分析了。

一些有趣的功能

當然,我們的表格有了rander選項之後,就已經將使用者操作完全解耦出來。這種情況下再為表格元件整合使用者操作相關的功能只是為了方便呼叫。

進度條

image

// table.jsx
...
<div class="table-body">
    {
        dataset.map((data, i)=>{
            return (
                <div className="table-row">
                    {
                        dataconf.map((conf, k) => {
                            return (
                                <div style={{ 'width': conf.width }}>
                                   ...
                                   <div className="d-il">
                                        {
                                            !conf.pipe ? (
                                                <span className="p-r z10">{data[conf.name]}</span>
                                            ) : null
                                        }
                                        <progress max="100" value={conf.progress && conf.progress(data)}
                                            className="progress-loading"></progress>
                                    </div>
                                   ...
                                </div>
                            )
                        })
                    }
                </div>
            )
        })
    }
</div>
...
複製程式碼
dataConfig=[{
    width: '10%',
    title: 'progress',
    width: '20%',
    progress: () => {
        //返回0-100表示進度百分比
        return 50
    },
}]
複製程式碼

氣泡提示

image

// table.jsx
...
<div class="table-body">
    {
        dataset.map((data, i)=>{
            return (
                <div className="table-row">
                    {
                        dataconf.map((conf, k) => {
                            return (
                                <div style={{ 'width': conf.width }}>
                                   ...
                                   <div class="pop-box">
                                        <div className="pop-toggle ptb4 mlr4">
                                            <div className="pop-main pr8">
                                                <div className="pop-content">
                                                    {conf.popup(data, i)}
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                   ...
                                </div>
                            )
                        })
                    }
                </div>
            )
        })
    }
</div>
...
複製程式碼
dataConfig=[{
    width: '10%',
    title: 'popup',
    width: '20%',
    popup: () => {
        return <button>氣泡提示</button>
    },
}]
複製程式碼

完整版請看這裡完整版Table元件&&所用到的樣式,覺得不錯的話不妨點個star哈哈。

注:樣式又是另一個話題了,可參看《Re從零開始的UI庫編寫生活之規範制定》

結尾

其實表格元件並不像想象中那麼難,只要理清楚思路,把該解耦的部分抽離出來,剩下的工作就是發揮想象力,往裡面新增各種功能而已。更多有趣的元件盡在SluckyUI中,歡迎多多交流,期待你的加入。

從零開始系列傳送門

相關文章