Ag Grid 的單元格合併 (Row Spanning)

邱秉誠
10 min readMay 25, 2024

--

Ag Grid 是一個構建資料表格的 Javascript 框架,它提供許多豐富靈活的配置,包括對整體表格以及單元格的樣式設定,或是提供不同型態的篩選和編輯功能等。

今天要為各位分享的是,如何利用 Row Spanning 來合併單元格,透過精簡表格的資料量,來增加可讀性。

情境

我們時常會遇到不同列中有許多重複的資訊,例如學生的考科成績資料表格中,同一名學生中的 name & gender & class 屬性是相同的,如果以原始表格呈現如下圖,資料會十分冗贅,如果可以將這些重複資訊合併成一個單元格,閱讀起來會舒適許多。

const data = [
{ name: "邱秉誠", gender: "男", class: "甲", subject: "國文", score: 80 },
{ name: "邱秉誠", gender: "男", class: "甲", subject: "英文", score: 82 },
{ name: "陳小英", gender: "女", class: "乙", subject: "國文", score: 95 },
{ name: "陳小英", gender: "女", class: "乙", subject: "英文", score: 80 },
{ name: "陳小英", gender: "女", class: "乙", subject: "數學", score: 78 },
]
學生成績資料表格因重複屬性而顯得冗贅

如何合併

我們透過 columnDefs 中的 自定義的 rowSpan 屬性來決定該 column 需要合併的列數,rowSpan 這個自定義 函式會對該 column 的 每筆資料都進行判斷,並回傳一個數字,若回傳 2 ,表示往下合併一個儲存格,如果是 3,就會往下合併兩個儲存格,以此類推。

const rowSpan = (params: any) => {
// params.data 表示這一筆 (列) 資料
// 回傳值表示該欄位的這格將往下合併多少儲存格
return params.data.rowspan
}

// 讓需要合併儲存格的欄位都附上 rowSpan 自定義函示
const [columnDefs, setColumnDefs] = useState([
{ field: "name", rowSpan: rowSpan},
{ field: "gender", rowSpan: rowSpan},
{ field: "class", rowSpan: rowSpan},
{ field: "subject"},
{ field: "score"},
]);

為了方便後續的 rowSpan 判斷,我們可以先整理一下原始資料,透過 transformeForRowSpan 函式讓原始資料都帶上 rowspan 數值,並且讓每組學生資料的首筆之外,欲被合併的欄位單元格 (name & gender & class) 都改為空字串,注意,要被合併的單元格值一定要改為其他值 (如空字串),否則無法生效!

整理後資料如下:

const data = [
{ name: "邱秉誠", gender: "男", class: "甲", subject: "國文", score: 80, rowspan:2 },
{ name: "", gender: "", class: "", subject: "英文", score: 82, rowspan:1 },
{ name: "陳小英", gender: "女", class: "乙", subject: "國文", score: 95, rowspan:3 },
{ name: "", gender: "", class: "", subject: "英文", score: 80,rowspan:1 },
{ name: "", gender: "", class: "", subject: "數學", score: 78,rowspan:1 },
]
 const transformeForRowSpan = (data: any) => {

const nameMap: any = {};

// 計算每個 name 出現的次數
data.forEach((row: any) => {
if (nameMap[row.name]) {
nameMap[row.name]++;
} else {
nameMap[row.name] = 1;
}
});

// 生成新的數據結構
const result: any = [];
data.forEach((row: any) => {
if (nameMap[row.name] > 0) {
result.push({ ...row, rowspan: nameMap[row.name] });
nameMap[row.name] = 0; // 後面該 name 的 row 將被清空
} else {
result.push({ ...row, name: "", gender: "", class: "", rowspan: 1 });
}
});

return result;
};

接著,我們便可以使用轉換後原始資料,來渲染在表格上了。這時你會發現,單元儲存格已經合併了,但是文字出現在左上角,我們可以透過客製化的 CellRenderer,以 Flex 布局,設定 justifyContent & alignItems 兩個屬性為 center,來達到將單元格水平跟垂直置中的效果

最初,合併後單元格內的文字會是從左上角排列
const MergedCellRender = (params: any) => {
return (
<div style={{
height: '100%', display: "flex", alignItems: "center",
backgroundColor: "#fff", justifyContent: "center"
}}>
{params.value}
</div >
)
}
套用 CellRenderer 後,會將儲存格內的文字水平 & 垂直 置中

完整程式碼

'use strict';
import 'ag-grid-community/styles/ag-grid.css';
import "ag-grid-community/styles/ag-theme-alpine.css";
import './app.css'
import { AgGridReact } from 'ag-grid-react';
import { useEffect, useRef, useState } from 'react';

const MergedCell = () => {
const data = [
{ name: "邱秉誠", gender: "男", class: "甲", subject: "國文", score: 80 },
{ name: "邱秉誠", gender: "男", class: "甲", subject: "英文", score: 82 },
{ name: "陳小英", gender: "女", class: "乙", subject: "國文", score: 95 },
{ name: "陳小英", gender: "女", class: "乙", subject: "英文", score: 80 },
{ name: "陳小英", gender: "女", class: "乙", subject: "數學", score: 78 },
]

const [rowData, setRowData] = useState<any[]>([]);

const transformeForRowSpan = (data: any) => {

const nameMap: any = {};

// 計算每個 name 出現的次數
data.forEach((row: any) => {
if (nameMap[row.name]) {
nameMap[row.name]++;
} else {
nameMap[row.name] = 1;
}
});

// 生成新的數據結構
const result: any = [];
data.forEach((row: any) => {
if (nameMap[row.name] > 0) {
result.push({ ...row, rowspan: nameMap[row.name] });
nameMap[row.name] = 0; // 後面該 name 的 row 將被清空
} else {
result.push({ ...row, name: "", gender: "", class: "", rowspan: 1 });
}
});

return result;
};

useEffect(() => {
// Transforme for row spanning
const newData = transformeForRowSpan(data)
setRowData(newData)
}, [])

const rowSpan = (params: any) => {
console.log(params.data.rowspan)
return params.data.rowspan
}

const MergedCellRender = (params: any) => {
return (
<div style={{
height: '100%', display: "flex", alignItems: "center",
backgroundColor: "#fff", justifyContent: "center"
}}>
{params.value}
</div >
)
}

const [columnDefs, setColumnDefs] = useState([
{ field: "name", rowSpan: rowSpan, cellRenderer: MergedCellRender, width: 100 },
{ field: "gender", rowSpan: rowSpan, cellRenderer: MergedCellRender, width: 100 },
{ field: "class", rowSpan: rowSpan, cellRenderer: MergedCellRender, width: 100 },
{ field: "subject", width: 100 },
{ field: "score", width: 100 },
]);

return (

<div
style={{ height: "400px" }}
className="ag-theme-alpine"
>
<AgGridReact
rowData={rowData}
columnDefs={columnDefs}
suppressRowTransform={true}
rowHeight={35}
/>
</div>
);
};
export default MergedCell;

參考資料

--

--

邱秉誠
邱秉誠

Written by 邱秉誠

畢業於台大工業工程所,目前任職於台積電。

No responses yet