仿製一個element tabel組建

最近接到一個新需求,一個表單組件, 要求橫向和縱向兩種模式,要求固定表頭和首列。這裏作個記錄。 首先呢, 固然是踩在巨人的肩膀上, 看看其餘的開源庫怎麼實現的固定表頭和首列,vue

1、結構分析

效果

拆分

代碼

分析

其實, 整個表單被分紅了三個部分, 每一種顏色是一個單獨的表格塊,拼接起來的, 每個部分都是用一個容器包裹着一個table表單, 用於限制寬高和處理滾動算法

<div class="wrapper"> 
    <table></table>
</div>
複製代碼
  • 黃色部分, 只渲染了表頭部分,外部容器overflo:hidden
  • 綠色部分就是核心, 渲染了tbody部分, 外部容器overflow: atuo, 監聽他的滾動條事件, 而後同步到固定表頭和固定列的滾動條
  • 藍色部分其實包含了了一個thead和tbody, 而後設置寬度等於首列的寬, overflow: hidden 而後絕對定位覆蓋在右邊的表單上面
  • 在body部分橫向滾動的時候, thead同步滾動, 在body垂直滾動的時候, 左邊的tbody也同步滾動

這裏我用了兩個子組件tableHead和tableBody來分別渲染thead和tbody部分,數組

這樣就比較清晰了。剩下的就是核心代碼編寫了。bash

2、實現

接下來遇到的一個問題就是多級表頭了。app

仔細分析這個dom結構。其實他是分紅三行來渲染的, 而後經過rowspan和colspan來實現的合併單元格 因此咱們須要經過算法, 將樹形的表頭數據攤開成一個二維數組, 而後每一個數據節點都須要算出rowspan和colspan。
這裏總結一下規律

  • 每個單元格的colsan應該是他所有子節點的總數,若是他不含子節點,則爲1
  • 每個單元格的rowspan分兩種狀況, 若是他不含子節點, 就是最大層數-當前曾是 + 1, 若是包含子節點,則爲1

丟代碼dom

// 提取所有的列
const getAllColumns = (columns) => {
  const result = [];
  columns.forEach((column) => {
    if (column.childs && column.childs.length) {
      result.push(column);
      result.push.apply(result, getAllColumns(column.childs));
      // result.push(...getAllColumns(column.childs))
    } else {
      result.push(column);
    }
  });
  return result;
}
複製代碼

這個函數的做用是將多個樹形結構組成的數組攤平 A、B分別是這個表頭的頂級單元格函數

// 從樹形整理成二維數組
const convertToRows = (originColumns) => {
  let maxLevel = 1;
  // 遞歸的算出每個子節點的層數, 和colspan
  const traverse = (column, parent) => {
    if (parent) {
      column.level = parent.level + 1;
      if (maxLevel < column.level) {
        maxLevel = column.level;
      }
    }
    if (column.childs && column.childs.length) {
      let colSpan = 0;
      column.childs.forEach((subColumn) => {
        traverse(subColumn, column);
        colSpan += subColumn.colSpan;
      });
      // 節點的colspan是子節點的和
      column.colSpan = colSpan;
    } else {
    // 不存在則爲一
      column.colSpan = 1;
    }
  };

  originColumns.forEach((column) => {
    column.level = 1;
    traverse(column);
  });
  const rows = [];
  for (let i = 0; i < maxLevel; i++) {
    rows.push([]);
  }
  // 轉化成一維數組的數據,
  // 注意, 在調用getAllColumns以前, 已經將每個節點設置好了level
  const allColumns = getAllColumns(originColumns);
  // 轉化成二維數組, 計算rowspan
  allColumns.forEach((column) => {
    if (!column.childs.length) {
      column.rowSpan = maxLevel - column.level + 1;
    } else {
      column.rowSpan = 1;
    }
    rows[column.level - 1].push(column);
  });

  return rows;
}
複製代碼

這一步將根據level將這個數組變成一個二維數組、每一層的節點在一個數組內ui

經過這麼整理過的數據就能夠直接經過循環渲染出來了。

其餘細節, 樣式的調整

  • colgroup和col能夠快速的格式化單元格,而col的個數就是這個table列的個數, 應該是每個樹的最終節點的和
  • 出現縱向滾動條以後會影響到總體寬度, 致使橫向混動thead和tbody不一樣步。產生錯位, 以下圖
    解決辦法是判斷當縱向滾動條存在的時候, 在thead添加一個單元格, 寬度爲滾動條的寬度

// 獲取滾動條寬度
export function getScrollbarWidth() {
  if (Vue.prototype.$isServer) return 0;
  if (scrollBarWidth !== undefined) return scrollBarWidth;
  const outer = document.createElement('div');
  outer.className = 'el-scrollbar__wrap';
  outer.style.visibility = 'hidden';
  outer.style.width = '100px';
  outer.style.position = 'absolute';
  outer.style.top = '-9999px';
  document.body.appendChild(outer);

  const widthNoScroll = outer.offsetWidth;
  outer.style.overflow = 'scroll';

  const inner = document.createElement('div');
  inner.style.width = '100%';
  outer.appendChild(inner);

  const widthWithScroll = inner.offsetWidth;
  outer.parentNode.removeChild(outer);
  scrollBarWidth = widthNoScroll - widthWithScroll;

  return scrollBarWidth;
};
複製代碼
  • 在出現橫向滾動條的時候, 左邊絕對定位會覆蓋一部分橫向滾動條, 以下 spa

    因此也要特殊處理, 限制左邊的tbody容器的高度減去一個滾動條的高度

  • 若是窗口寬度大於表格的理論寬度, 表格就會自動適應, 底部的tbody邊框了。而左邊絕對定位的寬度沒變, 會致使錯位, 因此要給table設置一個寬度, 這個寬度是能夠經過列數*每列的寬計算出來的prototype

  • 若是須要操做欄的話, 能夠經過vue的做用域插槽來實現, 將當列的數據拋出去就行了

這樣, 一個橫向的固定表頭首列就完成了, 實際上就是element table組建的劣化版。有時間再寫一下縱向表單的處理, 相對來講要複雜一下。

相關文章
相關標籤/搜索