Most tables and grids include a single header row that shows the name of the field that the column refers to. In many cases, the table data has a hierarchical nature, and it may be desirable to add several levels of column headers to expose the hierarchy. The W3 specification for the table element shows a simple example:
A test table with merged cells
/-----------------------------------------\
| | Average | Red |
| |-------------------| eyes |
| | height | weight | |
|-----------------------------------------|
| Males | 1.9 | 0.003 | 40% |
|-----------------------------------------|
| Females | 1.7 | 0.002 | 43% |
\\-----------------------------------------/
The "height" and "weight" columns are grouped under an "Average" header that makes the table easier to read. You can create this type of table using the FlexGrid (our JavaScript DataGrid), but it requires writing some code. Because this is a fairly common scenario, we wrote some functions that take an object describing the column hierarchy and set everything up automatically. Using these functions, you could create the table in the example above like this:
// define the data
// http://www.w3.org/TR/html401/struct/tables.html
var w3Data = [
{ gender: 'Males', height: 1.9, weight: 0.003, red: .4 },
{ gender: 'Females', height: 1.7, weight: 0.002, red: .43 },
];
// define the columns
var w3Columns = [
{ header: ' ', binding: 'gender' },
{
header: 'Average', columns: [
{ header: 'Height', binding: 'height', format: 'n1' },
{ header: 'Weight', binding: 'weight', format: 'n3' }
]
},
{ header: 'Red Eyes', binding: 'red', format: 'p0' }
];
// bind columns and data to a FlexGrid
bindColumnGroups(flex, w3Columns);
flex.itemsSource = w3Data;
The "w3Columns" array describes the column hierarchy using a plain JavaScript object. Each element in the array specifies the properties for a column on the grid, and each may have a "columns" property that specifies child columns. You may nest columns to any depth. The "bindColumnGroups" function creates the hierarchical column structure, including the columns themselves and the merged headers. Here is the result: To further illustrate, consider a table that compares the performance and composition of investment funds. You could have a group of columns to show performance and another to show composition:
// define the columns
var fundColumns = [
{ header: 'Name', binding: 'name', width: '2*' },
{ header: 'Curr', binding: 'currency', width: '*' },
{
header: 'Performance', columns: [
{ header: 'YTD', binding: 'perf.ytd', format: 'p2', width: '*' },
{ header: '1 M', binding: 'perf.m1', format: 'p2', width: '*' },
{ header: '6 M', binding: 'perf.m6', format: 'p2', width: '*' },
{ header: '12 M', binding: 'perf.m12', format: 'p2', width: '*' }]
},
{
header: 'Allocation', columns: [
{ header: 'Stocks', binding: 'alloc.stock', format: 'p0', width: '*' },
{ header: 'Bonds', binding: 'alloc.bond', format: 'p0', width: '*' },
{ header: 'Cash', binding: 'alloc.cash', format: 'p0', width: '*' },
{ header: 'Other', binding: 'alloc.other', format: 'p0', width: '*' }]
}
];
// bind columns and data to a FlexGrid
bindColumnGroups(s, fundColumns);
s.itemsSource = fundData;
Notice a few interesting points:
- The column data includes column properties such as “format” and “width”.
- The column data binds columns to sub-properties of the data items (e.g. “perf.ytd”).
And here is the result: To see both examples live and play with the code, please follow this link: http://jsfiddle.net/Wijmo5/gobtdg7t/ The “bindColumnGroups” function is easy to use and to customize. In the next few sections we will walk through the implementation so you will get a good understanding of how it works, what assumptions it makes, and how you can use or customize it to suit your needs. The “bindColumnGroups” function creates the columns, including their merged headers. It is implemented as follows:
// create column groups and set up grid headers
function bindColumnGroups(flex, columnGroups) {
// create the columns
flex.autoGenerateColumns = false;
createColumnGroups(flex, columnGroups, 0);
// merge the headers
mergeColumnGroups(flex);
// center-align headers vertically and horizontally
flex.formatItem.addHandler(function (s, e) {
if (e.panel == flex.columnHeaders) {
e.cell.style.display = 'table';
e.cell.innerHTML = '<div>' + e.cell.innerHTML + '</div>';
wijmo.setCss(e.cell.children[0], {
display: 'table-cell',
verticalAlign: 'middle',
textAlign: 'center'
});
}
});
}
The function starts by setting the grid’s autoGenerateColumns property to false. We will create the columns and don’t want the grid to do it for us when we give it a new itemsSource. Next, the function calls the createColumnGroups to create the columns and extra header cells, and the mergeColumnGroups function to merge the cells in the header. Finally, it uses the formatItem event to align the center-align the content of the header cells. The event handler sets the cell’s display style to “table” and places the cell content into a new div with display set to “table-cell”. This is an easy way to center the cell content vertically. The createColumnGroups function is implemented as follows:
function createColumnGroups(flex, columnGroups, level) {
// prepare to generate columns
var colHdrs = flex.columnHeaders;
// add an extra header row if necessary
if (level >= colHdrs.rows.length) {
colHdrs.rows.splice(colHdrs.rows.length, 0, new wijmo.grid.Row());
}
// loop through the groups adding columns or groups
for (var i = 0; i < columnGroups.length; i++) {
var group = columnGroups[i];
if (!group.columns) {
// create a single column
var col = new wijmo.grid.Column();
// copy properties from group
for (var prop in group) {
col[prop] = group[prop];
}
// add the new column to the grid, set the header
flex.columns.push(col);
colHdrs.setCellData(level, colHdrs.columns.length - 1, group.header);
} else {
// get starting column index for this group
var colIndex = colHdrs.columns.length;
// create columns for this group
createColumnGroups(flex, group.columns, level + 1);
// set headers for this group
for (var j = colIndex; j < colHdrs.columns.length; j++) {
colHdrs.setCellData(level, j, group.header);
}
}
}
}
The function loops through the elements in the columnGroups parameter. If a group has no child columns, the function add a column and initializes its properties from the group data. If a group does have child columns, the function calls itself recursively to create the child columns, then applies the group caption to the header cells for the whole group. At this point, the columns and their headers are ready, but they are not merged yet. This is the job of the mergeColumnGroups function:
function mergeColumnGroups(flex) {
// merge headers
var colHdrs = flex.columnHeaders;
flex.allowMerging = wijmo.grid.AllowMerging.ColumnHeaders;
// merge horizontally
for (var r = 0; r < colHdrs.rows.length; r++) {
colHdrs.rows[r].allowMerging = true;
}
// merge vertically
for (var c = 0; c < colHdrs.columns.length; c++) {
colHdrs.columns[c].allowMerging = true;
}
for (var c = 0; c < flex.topLeftCells.columns.length; c++) {
flex.topLeftCells.columns[c].allowMerging = true;
}
// fill empty cells with content from cell above
for (var c = 0; c < colHdrs.columns.length; c++) {
for (var r = 1; r < colHdrs.rows.length; r++) {
var hdr = colHdrs.getCellData(r, c);
if (!hdr || hdr == colHdrs.columns[c].binding) {
var hdr = colHdrs.getCellData(r - 1, c);
colHdrs.setCellData(r, c, hdr);
}
}
}
}
The function starts by setting the grid’s allowMerging property to “ColumnHeaders”. This assumes you don’t want the grid to merge the data cells. Next, the function enables merging on all column header rows and columns by setting their allowMerging property to true. We will not define a custom merge manager, so the grid will automatically merge header cells that have the same content. This assumes that you won’t have columns or groups with the same name (or if you do, at least they won’t be next to each other). The final block of code handles the situation where header cells have empty cells below them (this happens when groups have fewer levels than the maximum. In this case, the code simply copies the content from the cell above so they merge vertically.
FlexGrid is also available as an Angular DataGrid, React DataGrid and Vue DataGrid.