考虑到行跨度和列跨度,如何从一维数组创建动态 html 表?

Posted

技术标签:

【中文标题】考虑到行跨度和列跨度,如何从一维数组创建动态 html 表?【英文标题】:How would one create a dynamic html table from a one dimensional array, taking into account rowspan and colspan? 【发布时间】:2016-09-08 10:08:48 【问题描述】:

我需要从一个一维数组构造一个 html 表,为了抽象起见,它具有以下格式:

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

还有一个名为width 的属性将是动态的并表示列数。

我相信下面的代码很接近,并且可以处理“非行跨度”数据 - 但我在如何计算单元格跨越时遇到问题,而表格不会超过列数。

我觉得我需要一个“步进器”,每次有行跨度时它都会向上和向下计数,但我无法正确计算数学。

目前,任何行跨度都会导致下一行退出表格的右侧。

基本上我希望它能够将每一个都包裹起来并放在下一个可用的位置。换句话说,动态组装表格。

第 1 轮 - 不工作

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);

第 2 轮 - 改进,但假设输入有效

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(''));

编辑 - 输入错误

错误输入的一个例子是拆分单元格:

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;

【问题讨论】:

我在想当输入无效时会发生什么,我想不出能涵盖所有可能情况的东西。我唯一看到的是一条消息或某事说输入无效(可能是原因/在哪里)......您是否希望无效输入被“修复”?如果是这样,输入无效的方式有很多种,每种情况会发生什么? 理想情况下,“优雅地”捕获错误是最好的 - 我倾向于在失败的情况下将所有 col/row spans 设置为 1,以便表仍然可用,并显示错误用它。 由于您的数据是理想的,因此不太清楚您希望如何处理边缘情况。 IE。如果 a 中的行跨度位于 a2 上,而 colspan 为 2 的位于 b1 上,那么您要做什么? @Chris 所以基本上你的第二个解决方案是可以接受的,如果基本上有一个ValidateInput 函数来检查输入是否有效,即如果有人要添加该函数,这将是你的可接受的答案问题? 啊,是的,提供者输入是理想的。我现在将添加一个错误输入的示例 【参考方案1】:

这是 v0.0.1,它处理任何输入数据并构造 HTML 文本,前提是,就像在这种情况下,提供有意义的输入数据,因为垂直和水平跨越的单元格不相交或 colspan 不会' t 超出提供的width 值设置的限制。我还计划在以后开发一个 V0.0.2,无论存在随机 colspan 和 rowspan 值,它都能够生成有效的表格布局。我认为 v0.0.1 足以满足您的需求。

我首先开发了一个tableMap,它在二维数组中构造了表格的地图。实际上在客户端,现在构建 DOM 表相当容易。主单元格由一个名为 sp 的额外属性标记为 0,而跨单元格的 sp 属性为非零值。所以实际上 DOM 树在这个 2D 数组中很容易获得。只需反向迭代以选择具有sp == 0 的单元格,是构建 DOM 树唯一要做的事情。

但是,既然您要求 HTML 表格,对于服务器端,我更进一步,将 tableMap 转换为 HTML 字符串。

对不起,我的非正统缩进风格。我更喜欢使用箭头,三进制和短路很多,因此宽布局更容易让我感知代码。

供你玩的代码@repl.it

var 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 ,
],
    width = 3,
cellCount = input.reduce((p,c) => p += c.colspan * c.rowspan,0),
 rowCount = Math.ceil(cellCount/width),
       rc = r:0,c:0,
 tableMap = input.reduce((t,e) =>  var getNextRC = (rc) => rc.r = rc.c == 2 ? ++rc.r : rc.r;
                                                             rc.c = ++rc.c%width;
                                                             return rc,
                                       insertCell = (rc) =>  if (!t[rc.r][rc.c])
                                                                  for (var c = 0; c < e.colspan; c++)
                                                                  for (var r = 0; r < e.rowspan; r++)t[rc.r+r][rc.c+c] = "td": e, "sp": r+c;
                                                                getNextRC(rc);
                                                               else 
                                                              	getNextRC(rc);
                                                              	insertCell(rc);
                                                              
                                                              return rc;
                                                            ;
                                    rc = insertCell(rc);
                                    return t;, new Array(rowCount).fill(true).map(e => new Array(width).fill(false))),
tableHTML = tableMap.reduceRight((t,r,i) =>  var dt = r.reduceRight((t,d,i) => t = !d.sp ? i > 0 ? '</td><td colspan = "' + 
                                                                                                    d.td.colspan +
                                                                                                    '" rowspan = "' +
                                                                                                    d.td.rowspan +
                                                                                                    '">' + d.td.value + t
                                                                                                  : '<td colspan = "' + 
                                                                                                    d.td.colspan +
                                                                                                    '" rowspan = "' +
                                                                                                    d.td.rowspan +
                                                                                                    '">' + d.td.value + t
                                                                                          : t, '</td>');
                                              t = i > 0 ? '</tr><tr>' + dt + t
                                                        : '<tr>' + dt + t;
                                              return t;
                                            , '</tr>');

document.write("<style>table, th, td border: 1px solid black;</style>");
document.write('<table>' + tableHTML + '</table>');

【讨论】:

【参考方案2】:

这是问题的直接解决方案。

    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);
    

更新:

如果我添加 css,那么表格会按预期(期望)呈现。

    .tblborder:solid 1px #ccc
    .tbl trheight:20px
    .tbl tdborder:solid 1px #fcc

生成的 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>

更新 2

如果你有足够的内容,那么就不需要固定高度的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:

    .tblborder:solid 1px #ccc;width:300px
    /*.tbl trheight:20px*/
    .tbl tdborder:solid 1px #fcc

更何况,.tbl trheight:20px 没有任何作用。

【讨论】:

当所有单元格的行跨度大于1 时跳过行。在此处查看单元格a3b1c2 jsbin.com/qoxacu/edit?html,output 我更新了答案,然后发现 @Mauricio Poppe 提供了相同的修复 (tr height:20px)【参考方案3】:

我认为您的替代解决方案是正确的,应该验证的两个极端情况是

一个单元格可能被渲染出边界,例如当单元格的起始位置 + 它的 colspan 大于允许的 width 时(蓝色单元格被渲染出边界)

可能会在已占用的位置渲染单元格(蓝色单元格试图占用红色单元格占用的空间)

我想出了以下算法,它与您的第二个解决方案非常相似

创建一个N行和width列的矩阵,N的值将在需要时分配 对于输入中的每个cell 从矩阵的第一行开始从左向右移动以尝试找到空白空间,请注意,如果当前行中没有空白空间,则会在此处分配新行 设ij为矩阵中第一个空格的行列,那么我们需要占用下面的i + cell.rowspace乘以j + cell.colspace单元格,在实现中我使用了单元格的索引 如果cell 试图占用超出范围的单元格,则会引发错误 如果cell 试图占用矩阵中已经保存了一些值的单元格,则会引发错误

实现如下

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


更新:用户在 cmets 中发布了一个似乎无法正常渲染的案例,上面的算法适用于这种情况,即使标记看起来不错,但它看起来像是此表中的一行被渲染的高度为零,我相信有很多方法可以解决这个问题,我通过在table tr 元素上设置一个固定高度来修复它

Demo fixing the problem where a &lt;tr&gt; was rendered with a height = 0

【讨论】:

有趣的解决方案。但是,它将跳过仅包含行跨度大于1 的单元格的行。例如:jsbin.com/faqosenowu/edit?js,output 其中a3c2d1 都应该有 2 行的高度。 ty 对于反馈@C14L,我首先不明白表格有什么问题,因为标记很好(我检查了tablesgenerator.com/html_tables,你的示例的标记输出是相同的),但是我注意到其中一行的渲染没有高度,我添加了一个“哨兵”单元格作为每行的第一个单元格以避免这种怪癖jsbin.com/lulevoc/edit?css,js,console,output 解决这个问题的另一个简单方法是在table tr元素上设置一个固定高度,更新jsbin链接 同时,如果您查看jsbin.com/qoxacu/edit?html,output,您会注意到 tr 高度不是 0。它大约是 1px(边框宽度左右)。手动创建这样的表结构时也是如此。

以上是关于考虑到行跨度和列跨度,如何从一维数组创建动态 html 表?的主要内容,如果未能解决你的问题,请参考以下文章

如何将自动完成或 keydown 绑定到动态创建的跨度?

如何在 Ag-Grid 中将相同的相邻行与动态行跨度合并? [关闭]

RecyclerView GridLayoutManager:如何自动检测跨度计数?

Javascript DOM如何隐藏不合适的复选框

如何从签入和签出列表中计算时间跨度?

根据项目位置动态更改 StaggeredGridLayout 的列跨度计数