5
\$\begingroup\$

I will be given the following input:

const input = [
 { value: "a1", colspan: 1, rowspan: 1 }, 
 { value: "a2", colspan: 1, rowspan: 1 }, 
 { value: "a3", colspan: 1, rowspan: 3 }, 
 { value: "b1", colspan: 1, rowspan: 1 }, 
 { value: "b2", colspan: 1, rowspan: 1 }, 
 { value: "c1", colspan: 1, rowspan: 1 }, 
 { value: "c2", colspan: 1, rowspan: 2 }, 
 { value: "d1", colspan: 1, rowspan: 1 }, 
 { value: "d3", colspan: 1, rowspan: 1 }, 
 { value: "e1", colspan: 1, rowspan: 1 }, 
 { value: "e2", colspan: 2, rowspan: 1 }, 
];
const width = 3;

And from such, I need to construct an HTML table, taking into account colspan and rowspans. The resulting table would look like:

*----+----+----*
| a1 | a2 | a3 |
+----+----+ |
| b1 | b2 | |
+----+----+ |
| c1 | c2 | |
+----+ +----+
| d1 | | d2 |
+----+----+----+
| e1 | e2 |
*----+---------*

This works, but I'm not sure if it's the best approach or if it could be tackled better:

// Define the total possible cell count
// This poorly assumes no gaps 
const totalCellCount = reduce(input, (sum, c) => sum + (c.colSpan * c.rowSpan), 0);
// Fill a 2d array with placeholders so the below section 
// can find the next available "spot". Chunk is being 
// used to create the 2d array
const grid = chunk(fill(new Array(totalCellCount), -1), columnCount);
each(input, cell => {
 // A default "not found" state. 
 let start = { x: -1, y: -1 };
 // Search for the next available spot working from 
 // left to right, top to bottom
 outerLoop: for(let y = 0; y < grid.length; y++) {
 for(let x = 0; x < columnCount; x++) {
 if(grid[y][x] === -1) {
 // Found our starting position, save 
 // it and exit out
 start = { x, y };
 break outerLoop;
 }
 }
 }
 // No more spots. There is no scenario where it 
 // could only be -1 on x or y, so just check 
 // the `x`
 if(start.x === -1) {
 return false;
 }
 // Fill area with null basically saying, don't 
 // do anything in this area when we access 
 // the grid later for processing
 for(let y = 0; y < cell.rowSpan; y++) {
 for(let x = 0; x < cell.colSpan; x++) {
 grid[start.y + y][start.x + x] = null;
 }
 }
 // Except the upper left spot, that is the actual 
 // cell. The final result for a cell of 2 x 3 
 // would look like:
 //
 // +------+------+ 
 // | cell | null |
 // | null | null |
 // | null | null |
 // +------+------+ 
 //
 grid[start.y][start.x] = cell;
});
// At this point, we now have a grid populated 
// with workable data - cells and null based
// on the surface area of each. Lets convert
// this to html table td's + tr's. This, 
// imo, is the "easy" bit
let trs = [];
let tds = [];
for(let y = 0; y < grid.length; y++) {
 for(let x = 0; x < grid[y].length; x++) {
 // Fetch the cell at this location. If 
 // it is null, its the consumed space
 // of a colspan or a rowspan, so we
 // can just ignore it
 const cell = grid[y][x];
 if(cell) {
 const { value, rowspan, colspan } = cell;
 tds.push('<td colspan="'+colspan+'" rowspan="'+rowspan+'">'+value+'</td>');
 }
 } 
 // End of this row, assemble and push into 
 // the tr array
 trs.push('<tr>'+tds.join('')+'</tr>');
 // Reset the td array for the next row
 tds = [];
}
// Simple join and attach to screen
$(".table").append(trs.join(''));

JS Bin

I'm using ES6/ES2015, although I could use template strings. Ignore them for now because it seems to freak out jsbin.

The data set is reasonably small, so although performance improvements are welcome, stability and edges cases would be preferred.

Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked May 13, 2016 at 3:43
\$\endgroup\$
1
  • \$\begingroup\$ What do you mean by edge case? Can the input data include bad rowspan (intersects with columns of below rows) and colspan (extends beyond the width of the table) values..? \$\endgroup\$ Commented Jun 1, 2016 at 11:30

1 Answer 1

2
\$\begingroup\$

I really wanted to find a purely functional solution to this problem, but I wasn't able to. Nevertheless, your basic idea is pretty simple, and can be made even shorter and more declarative:

// Cell calculations as pure data. "cells" is the variable containing
// all cells, which we are filling in
var width = 3,
 numCells = input.reduce((m,x) => m += x.rowspan + x.colspan -1, 0),
 cells = new Array(numCells).fill(false),
 dummy = { dummy: true}, 
 addAcross = (i,n) => R.times(j => cells[i+j+1] = dummy, n),
 addDown = (i,n) => R.times(j => cells[i+(j+1)*width] = dummy, n),
 nextIndex = () => cells.findIndex(x => !x);
 // Everything happens here. We fill in "cells" with the real
 // cells and dummy cells for the span cells
 input.forEach(x => {
 var i = nextIndex();
 cells[i] = x;
 addAcross(i, x.colspan-1);
 addDown(i, x.rowspan-1);
 })
// HTML View Helpers
// Once we have the data, just split the array into subarrays
// for each row, and filter out the dummy cells. In this format
// constructing the html itself is trivial
var rows = R.splitEvery(width, cells).map(r => r.filter(x => !x.dummy)),
 asTd = c => `<td rowspan=${c.rowspan} colspan=${c.colspan}>${c.value}</td>`,
 asTr = row => `<tr>${row.map(asTd).join('')}</tr>`,
 asTable = rows => `<table border=1>${rows.map(asTr).join('')}</table>`;
document.querySelector('body').innerHTML = asTable(rows);

Note: I used one ramda library function just to break the array into subarrays. Lodash has a similar one, but I prefer ramda.

Here's a working bin of the final solution: http://jsbin.com/nimuca/2/edit?js,output

answered May 14, 2016 at 18:17
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.