Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How would one create a dynamic html table from a one dimensional array, taking into account rowspan and colspan?

I need to construct a html table from a one dimensional array which, for abstractions sake, has the following format:

{ value: "ABC", colspan: 1, rowspan: 2 }, // etc

There is also a property called width which will be dynamic and represent the number of columns.

The code below, I believe is close, and can handle "non-rowspan" data - but I am getting tripped up on how to account for cells spanning, without the table exceeding the column count.

I feel like I need a "stepper" which counts up and down everytime there is a rowspan, but I can't get the maths correct.

At the moment, any rowspan causes the next row to exit the right of the table.

Essentially I would like it to wrap and drop each one in the next available spot. In otherwords assmeble the table dynamically.

Round 1 - Not working

http://jsbin.com/zopoxaqato/edit?js,console,output

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;



const trs = [];
let tds = [];
let rowSpanOffset = 0;

// Loops over entries
input.forEach((cell, index) => {

  // Stock standard td
  tds.push(`<td colspan="${cell.colspan}" rowspan="${cell.rowspan}">${cell.value}</td>`);


  // New row time
  if(index % width === width - 1 || rowSpanOffset < 0) {
    trs.push("<tr>" + tds.join('') + "</tr>");
    // Reset for next row
    tds = [];    
  }

});


const leTable = "<table class='table'>"+trs.join('')+"</table>";


$("body").append(leTable);

Round 2 - Improved, but assumes input is valid

http://jsbin.com/solesiyuro/edit?js,output

const input = [
  { value: "a1", colspan: 1, rowspan: 1 }, // 1
  { value: "a2", colspan: 1, rowspan: 1 }, // 2
  { value: "a3", colspan: 1, rowspan: 3 }, // 3

  { value: "b1", colspan: 1, rowspan: 1 }, // 1
  { value: "b2", colspan: 1, rowspan: 1 }, // 1

  { value: "c1", colspan: 1, rowspan: 1 }, // 1
  { value: "c2", colspan: 1, rowspan: 2 }, // 2

  { value: "d1", colspan: 1, rowspan: 1 }, // 1
  { value: "d3", colspan: 1, rowspan: 1 }, // 1

  { value: "e1", colspan: 1, rowspan: 1 }, // 1
  { value: "e2", colspan: 1, rowspan: 1 }, // 2
];
const width = 3;

const totalCellCount = _.reduce(input, (sum, c) => sum + c.colspan * c.rowspan, 0);
const grid = _.chunk(_.fill(new Array(totalCellCount), -1), width);

_.each(input, cell => {
  let start = [-1, -1];

  outerLoop: 
  for(let y = 0; y < grid.length; y++) {
      for(let x = 0; x < width; x++) {
          if(grid[y][x] === -1) {
            start = [x, y];
            break outerLoop;
          }
      }    
  }

  for(let y = 0; y < cell.rowspan; y++) {
      for(let x = 0; x < cell.colspan; x++) {
         grid[start[1] + y][start[0] + x] = null;        
      }    
  }
  grid[start[1]][start[0]] = cell;        

});

let trs = [];
let tds = [];

for(let y = 0; y < grid.length; y++) {
  for(let x = 0; x < grid[y].length; x++) {
    const cell = grid[y][x];
    if(cell) {
      const value = cell.value;
      tds.push('<td colspan="'+cell.colspan+'" rowspan="'+cell.rowspan+'">'+cell.value+'</td>');
    }
  }    
  trs.push('<tr>'+tds.join('')+'</tr>');
  tds = [];
}


$(".table").append(trs.join(''));

Edit - Bad input

An example of bad input would be splitting cells:

const input = [
  { value: "a1", colspan: 1, rowspan: 1 },
  { value: "a2", colspan: 1, rowspan: 2 },
  { value: "a3", colspan: 1, rowspan: 1 },

  { value: "b1", colspan: 3, rowspan: 1 },

];
const width = 3;
like image 758
Chris Avatar asked May 12 '16 13:05

Chris


People also ask

How do I create a Rowspan table in HTML?

Specifies the number of rows a cell should span. Note: rowspan="0" tells the browser to span the cell to the last row of the table section (thead, tbody, or tfoot). Chrome, Firefox, and Opera 12 (and earlier versions) support rowspan="0".

How do I use Rowspan and colspan together in HTML?

You can use rowspan="n" on a td element to make it span n rows, and colspan="m" on a td element to make it span m columns. Looks like your first td needs a rowspan="2" and the next td needs a colspan="4" . Show activity on this post. Show activity on this post.


2 Answers

I think you were on the right track with your alternative solution, the two corner cases that should be validated are

  • a cell might be rendered out of bounds e.g. when a cell's start position + its colspan is bigger than the width allowed (the blue cell is rendered out of bounds)

out of bounds

  • a cell might be rendered in a place already occupied (the blue cell tries to occupy a space taken by the red cell)

cell occupied

I came up with the following algorithm which is very similar to your second solution

  • Create a matrix of N rows and width columns, the value of N will be allocated whenever needed
  • For each cell in your input
    • Move from left to right starting from the first row of the matrix trying to find an empty space, note that this is where the allocation of new rows occur if there wasn't an empty space in the current row
    • Let i and j be the row and column of the first empty space in the matrix, then we need to occupy the following i + cell.rowspace times j + cell.colspace cells, In the implementation I use the index of cell
    • If by any means cell tries to occupy an out of bound cell throw an error
    • If by any means cell tries to occupy a cell in the matrix which already has some value saved throw an error

The implementation looks as follows

class Matrix {
  constructor(width) {
    this.width = width
    this.data = []
  }

  set(i, j, d) {
    if (j >= width) throw Error(`set was run out of bounds index (${i}, ${j})`)
    var value = this.get(i, j)
    if (value !== undefined) throw Error(`cell (${i}, ${j}) is occupied with ${value}`)
    this.data[i][j] = d
  }

  get(i, j) {
    this.data[i] = this.data[i] || Array(this.width)
    return this.data[i][j]
  }

  findNextEmpty(i, j) {
    while (true) {
      if (this.get(i, j) === undefined) {
        return [i, j]
      }
      j += 1
      if (j === this.width) {
        i += 1
        j = 0
      }
    }
  }

  fromData(data) {
    let i = 0
    let j = 0
    data.forEach((meta, metaIndex) => {
      [i, j] = this.findNextEmpty(i, j)
      for (var ci = i; ci < i + meta.rowspan; ci += 1) {
        for (var cj = j; cj < j + meta.colspan; cj += 1) {
          this.set(ci, cj, metaIndex)
        }
      }
    })
    return this.data
  }  
}

try {
  const table = new Matrix(width).fromData(input)
} catch (err) {
  // the input was invalid
}

Demo


Update: A user has posted a case in the comments which seemed not to render fine, the algorithm above works for this case, even the markup looks fine however it seems like a row in this table was rendered with a height equal to zero, I'm sure there are a lot of ways to fix this, I fixed it by setting a fixed height over the table tr elements

Demo fixing the problem where a <tr> was rendered with a height = 0

like image 72
Mauricio Poppe Avatar answered Sep 18 '22 00:09

Mauricio Poppe


This is straightforward solution of the question.

    function buildTbl() {
        var tbl = document.createElement('table');
        tbl.className = 'tbl';
        var cols = width, tr = null, td = null, i = 0, inp = null, rowspan = [];
        while (inp = input[i]) {
            if (cols >= width) {
                tr = tbl.insertRow(-1);
                cols = 0;
                for (var j = 0, n = rowspan.length; j < n; j++) {
                    if (rowspan[j] > 1) {
                        cols++;
                        rowspan[j]--;
                    }
                }
            }
            td = tr.insertCell(-1);
            td.innerHTML = inp.value;
            if (inp.colspan > 1)
                td.setAttribute('colspan', inp.colspan);
            if (inp.rowspan > 1) {
                td.setAttribute('rowspan', inp.rowspan);
                rowspan.push(inp.rowspan);
            }
            cols += inp.colspan;
            i++;
        }
        document.getElementById('content').appendChild(tbl);
    }

Update:

If I add css then the table is rendered as expected (desired).

    .tbl{border:solid 1px #ccc}
    .tbl tr{height:20px}
    .tbl td{border:solid 1px #fcc}

Generated HTML:

<table class="tbl">
    <tbody>
        <tr>
            <td>a1</td>
            <td>a2</td>
            <td rowspan="3">a3</td>
        </tr>
        <tr>
            <td rowspan="2">b1</td>
            <td>b2</td>
        </tr>
        <tr>
            <td rowspan="2">c2</td>
        </tr>
        <tr>
            <td>d1</td>
            <td>d3</td>
        </tr>
        <tr>
            <td>e1</td>
            <td colspan="2">e2</td>
        </tr>
    </tbody>
</table>

Update 2

If you have enough content then there is no need for fixed height of tr.

        const input = [
  { value: "a1 long content long content long content long content long content long content long content ", colspan: 1, rowspan: 1 },
  { value: "a2 long content long content long content long content long content long content", colspan: 1, rowspan: 1 },
  { value: "a3 long content long content long content long content long content long content", colspan: 1, rowspan: 3 },

  { value: "b1 long content long content long content long content long content long content long content long content long content long content", colspan: 1, rowspan: 2 },
  { value: "b2 long content long content long content long content long content long content", colspan: 1, rowspan: 1 },

 // { value: "c1", colspan: 1, rowspan: 1 },
  { value: "c2 long content long content long content long content long content long content long content", colspan: 1, rowspan: 2 },

  { value: "d1 long content long content long content long content long content long content", colspan: 1, rowspan: 1 },
  { value: "d3 long content long content long content long content long content long content", colspan: 1, rowspan: 1 },

  { value: "e1 long content long content long content long content long content", colspan: 1, rowspan: 1 },
  { value: "e2 long content long content long content long content long content long content", colspan: 2, rowspan: 1 },
              ];

Css:

    .tbl{border:solid 1px #ccc;width:300px}
    /*.tbl tr{height:20px}*/
    .tbl td{border:solid 1px #fcc}

Even more, .tbl tr{height:20px} has no effect.

like image 39
Alex Kudryashev Avatar answered Sep 22 '22 00:09

Alex Kudryashev