簡述用React實現Table元件

weixin_34232744發表於2019-02-15

一、背景

由於剛到現在就職的公司接手到公司後臺的專案程式碼的時候,發現系統中大量使用了表格,因為程式碼經多人蔘與過,導致有些程式碼塊是通過ul>li標籤佈局有些地方則是通過div佈局等等,導致了程式碼的嚴重冗餘;簡單的表格還好,如果是表格中需要對某些欄位進行排序的話,又會大大增加程式碼的量級和可讀性,如圖


程式碼為:


從上圖部分截圖可以看出,一個表格的程式碼量大概能佔到200行程式碼,而且每個欄位的排序的上下箭頭都會增加兩個state去進行管理,5個欄位需要排序則需要增加10個state,也就意味著每次我們在進入路由元件的時候和進行各種重新整理重置操作的時候都需要去處理這10個state等等,還有一個問題就是表格的佈局,每個欄位所佔的寬和高度都是class去設定的,也就意味著,每次產品來告訴你,這個表格需要增加一個欄位,你也就得重新去計算所有的欄位所佔寬度和分配適合的寬度。以上所述的問題違背了程式開發的耦合性和複用性,所以決定封裝一個Table元件去解決這個問題。

二、分析需要實現Table的API

1、傳入一個陣列自動構建出表頭以及該表頭下這一列的顯示的內容,命名為columns,資料型別為array
2、傳入表格需要顯示的資料來源陣列,命名為dataSource,資料型別為array
3、預設狀態下,表格需要展示的內容,命名為emptyText,資料型別為string
4、傳入表格的分頁項,包含當前頁碼,總頁碼,以及分頁器的點選事件,命名為pagination,資料型別為object
5、傳入一個表格每行勾選的配置物件,包含是否使用勾選、勾選的點選事件,便於滿足對錶格勾選項進行批量處理的需求,命名為rowSelection,資料型別為object
6、傳入一個表格行的點選事件,便於滿足點選當前行進入詳情的需求,命名為onRowClick,資料型別為function
7、傳入表格的樣式物件,給表格內部的外層包裹容器新增行內樣式,命名為style,資料型別為object
8、傳入一個className給表格內部的外層包裹容器新增className,可以實現在表格元件外部設定表格的每一列的className,資料型別為string

三、具體實現

1、整體程式碼

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ListNone from '../ListNone/ListNone';
import Pager from '../Pager/Pager';
import './Table.less';
// API
// columns為表格定義資料格式,title欄位為表格標題,dataIndex為傳入的資料來源中需要顯示的欄位一致,可以通過render函式來渲染當前列的資料 -> Array// dataSource為資料來源 -> Array
// rowSelection列表項是否可選 -> Object | null
// pagination為分頁器 -> Object | false
// onRowClick為單行點選事件

export default class Table extends Component {  
    constructor(props) {    
        super(props);
        this.state = { 
            rowAllSelect: false,//全選按鈕  
            rowCheck: [],//單項勾選框   
            rowSelId: [], //選中的id    
            sortArr: [], //排序標誌陣列  
        };  
    }  

    static propTypes = {    
        columns: PropTypes.array.isRequired, //表頭名稱
        dataSource: PropTypes.array.isRequired, //資料列表
        emptyText: PropTypes.string, //列表為空時表格預設狀態
        pagination: PropTypes.object, //表格分頁物件,包含當前頁碼,總共頁數,分頁器的點選事件,
        rowSelection: PropTypes.object, //表格單選全選的配置物件,onChange為單選全選框的狀態改變事件,可以得到選中的列的資料    style: PropTypes.object, //表格的樣式物件
        isLastNoOp: PropTypes.bool //表格最後一行不需要渲染操作樣式  
    }

    static defaultProps = {
        dataSource: [],
        columns: [],
        pagination: {},
        emptyText: '暫無相關資訊'
    }; 

    componentWillReceiveProps(nextProps) {    
        let { dataSource } = nextProps;
        let { rowCheck } = this.state;
        let sortArr = [];
        if (dataSource != this.props.dataSource) {
            dataSource.map((item, i) => {
                rowCheck[i] = false;
            });
            this.props.columns.map(item => {
                sortArr.push("");
            });
            const rowSelId = [];
            this.props.rowSelection && this.props.rowSelection.onChange(rowSelId);
            this.setState({
                rowAllSelect: false,//全選按鈕
                rowCheck,//單項勾選框
                rowSelId, //選中的id
                sortArr
            });
        }
  }

  //單選
  goodsChange = (item, index, event) => {
    let { rowCheck } = this.state;
    let status = false;
    let { rowSelId } = this.state;
    if (event.target.checked) {
      rowCheck[index] = true;
      rowSelId.push(item);
    } else {
      rowCheck[index] = false;
      for (let i = 0; i < rowSelId.length; i++) {
        if (rowSelId[i].id == item.id) {
          rowSelId.splice(i, 1);
          break;
        }
      }
      // rowSelId.splice($.inArray(item.id,rowSelId),1);
    }
    for (let i = 0; i < rowCheck.length; i++) {
      if (rowCheck[i]) {
        status = true;
      } else {
        status = false;
        break;
      }
    }
    if (status) {
      let all = [];
      for (let i = 0; i < rowCheck.length; i++) {
        all.push(true);
      }
      rowCheck = all;
    }
    this.setState({
      rowCheck,
      rowAllSelect: status,
      rowSelId
    });
    this.props.rowSelection && this.props.rowSelection.onChange(rowSelId);
  }

  //全選  goodsAllChange = (event) => {
    var all = [];
    var rowSelId = [];
    const { dataSource } = this.props;
    var status = false;
    if (event.target.checked) {
      for (var i = 0; i < dataSource.length; i++) {
        all.push(true);
        rowSelId.push(dataSource[i]);
      }
      status = true;
    } else {
      for (var i = 0; i < dataSource.length; i++) {
        all.push(false);
        rowSelId = [];
      }
      status = false;
    }
    this.setState({
      rowCheck: all,
      rowAllSelect: status, 
      rowSelId
    });
    this.props.rowSelection && this.props.rowSelection.onChange(rowSelId);
    event.stopPropagation();
  }
  trClick = (data, event) => {
    this.props.onRowClick && this.props.onRowClick(data, event);
  }
  sort = (item, type, index, event) => {
    let { sortArr } = this.state;
    for (let i = 0; i < sortArr.length; i++) {
      sortArr[i] = "";
    }
    sortArr[index] = type;
    this.setState({
      sortArr
    });
    item.sorter(type, item.dataIndex);
  }

  //列篩選
  colFilter = (item,event) =>{
    item.colFilter.eventL(event.target.value);
  }

  //遞迴columns的children
  retColumns = (columns) => {
    const { sortArr, rowAllSelect } = this.state;
    const { rowSelection, bordered } = this.props;
    return (
      <thead className={bordered && 'bordered'}>
        {this.convertToRows(columns).map((row,j) =>{
          return (
            <tr key={j}>
            {rowSelection && <th><input type="checkbox" className={rowAllSelect ? 'checked' : ''} checked={rowAllSelect} onChange={this.goodsAllChange} /></th>}
            {row.map((item, i) => {
              return (
                <th key={i} {...item} className={bordered && 'bordered'}>
                  <div className={item.sorter?"marginL":""}>{item.title}</div>
                  {item.sorter &&
                    <div>
                      <span className={`sort up${sortArr[i] === 'asc' ? ` top` : ``}`} onClick={this.sort.bind(this, item, "asc", i)}></span>
                      <span className={`sort down${sortArr[i] === 'desc' ? ` bottom` : ``}`} onClick={this.sort.bind(this, item, "desc", i)}></span>
                    </div>
                  }
                  {item.colFilter &&
                     <span className="colFilter">
                      <select onChange={this.colFilter.bind(this,item)}>
                      {
                        item.colFilter.data.map((filterItem,k) =>{
                          return (<option value={filterItem.val} key={k}>{filterItem.name}</option>)
                        })
                      }
                      </select>
                    </span>
                  }
                </th>
              )
            })}
            </tr>
          )
        })}
      </thead>
    )
  }

  getAllColumns = (columns) => {
    const result = [];
    columns.forEach((column) => {
      if (column.children) {
        result.push(column);
        result.push.apply(result, this.getAllColumns(column.children));
      } else {
        result.push(column);
      }
    });
    return result;
  }

  convertToRows = (originColumns) => {
    let maxLevel = 1;
    const traverse = (column, parent) => {
      if (parent) {
        column.level = parent.level + 1;
        if (maxLevel < column.level) {
          maxLevel = column.level;
        }
      }
      if (column.children) {
        let colSpan = 0;
        column.children.forEach((subColumn) => {
          traverse(subColumn, column);
          colSpan += subColumn.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([]);
    }
    const allColumns = this.getAllColumns(originColumns);
    allColumns.forEach((column) => {
      if (!column.children) {
        column.rowSpan = maxLevel - column.level + 1;
      } else {
        column.rowSpan = 1;
      }
      rows[column.level - 1].push(column);
    });
    return rows;
  };

  retRows = (columns, data, index, isLastNoOp, length) =>{
    return (
      this.getAllColumns(columns).map((col, i) => {
        if (col.dataIndex !== undefined) {
          if (data[col.dataIndex] !== undefined) {
            return (<td key={i} width={col.width && col.width}>{col.render ? col.render(data[col.dataIndex], data, index) : data[col.dataIndex]}</td>)
          } else {
            return (<td key={i} width={col.width && col.width}></td>)
          }
        } else {
          if (col.children && col.children.length > 0 ) {
            this.retRows(col.children, data);
          } else {
            let renderEle = "";
            if (col.render) {
              renderEle = col.render(data, index);
              if(isLastNoOp && index === length - 1){
                renderEle = "";
              }
            }
            return (<td key={i} width={col.width && col.width}>{col.render && renderEle}</td>);
          }
        }
      })
    );
   }

  render () {
    const {
      className,
       columns,
       dataSource,
       pagination,
       rowSelection,
       style,
       emptyText,
       isLastNoOp
    } = this.props;
    const { rowCheck } = this.state;
    return (
      <div className={`table-box ${className}`}>
        <table style={style}>
          {/* 表頭部分 */}
          {this.retColumns(columns)}
          <tbody>
            {
              dataSource.map((data, i) => {
                return (
                  <tr key={i} onClick={this.trClick.bind(this, data)}>
                    {rowSelection && <td><input type="checkbox" className={rowCheck[i] ? 'checked' : ''} checked={rowCheck[i]} onChange={this.goodsChange.bind(this, data, i)} /></td>}
                    {this.retRows(columns, data, i, isLastNoOp, dataSource.length)}
                  </tr>
                )
              })
            }
          </tbody>
        </table>
        <ListNone list={dataSource} text={emptyText} />
        <Pager {...pagination} />
      </div>
    );
  }}複製程式碼

2、表頭部分(thead)

  //遞迴columns的children 
 retColumns = (columns) => {
    const { sortArr, rowAllSelect } = this.state;
    const { rowSelection, bordered } = this.props;
    return (
      <thead className={bordered && 'bordered'}>
        {this.convertToRows(columns).map((row,j) =>{
          return (
            <tr key={j}>
            {rowSelection && <th><input type="checkbox" className={rowAllSelect ? 'checked' : ''} checked={rowAllSelect} onChange={this.goodsAllChange} /></th>}
            {row.map((item, i) => {
              return (
                <th key={i} {...item} className={bordered && 'bordered'}>
                  <div className={item.sorter?"marginL":""}>{item.title}</div>
                  {item.sorter &&
                    <div>
                      <span className={`sort up${sortArr[i] === 'asc' ? ` top` : ``}`} onClick={this.sort.bind(this, item, "asc", i)}></span>
                      <span className={`sort down${sortArr[i] === 'desc' ? ` bottom` : ``}`} onClick={this.sort.bind(this, item, "desc", i)}></span>
                    </div>
                  }
                  {item.colFilter &&
                     <span className="colFilter">
                      <select onChange={this.colFilter.bind(this,item)}>
                      {
                        item.colFilter.data.map((filterItem,k) =>{
                          return (<option value={filterItem.val} key={k}>{filterItem.name}</option>)
                        })
                      }
                      </select>
                    </span>
                  }
                </th>
              )
            })}
            </tr>
          )
        })}
      </thead>
    )
  }複製程式碼

注意:convertToRows函式是為了處理多表頭的情況,接收我們傳入Table元件的columns,通過遞迴columns中每一項的children欄位並判定每一列的colSpan,最終返回一個新的columns進行表頭的渲染

3、表體部分(tbody)

  retRows = (columns, data, index, isLastNoOp, length) =>{
    return (
      this.getAllColumns(columns).map((col, i) => {
        if (col.dataIndex !== undefined) {
          if (data[col.dataIndex] !== undefined) {
            return (<td key={i} width={col.width && col.width}>{col.render ? col.render(data[col.dataIndex], data, index) : data[col.dataIndex]}</td>)
          } else {
            return (<td key={i} width={col.width && col.width}></td>)
          }
        } else {
          if (col.children && col.children.length > 0 ) {
            this.retRows(col.children, data);
          } else {
            let renderEle = "";
            if (col.render) {
              renderEle = col.render(data, index);
              if(isLastNoOp && index === length - 1){
                renderEle = "";
              }
            }
            return (<td key={i} width={col.width && col.width}>{col.render && renderEle}</td>);
          }
        }
      })
    );
   } 複製程式碼


4、表格less部分

.table-box{
  table{
    width: 100%;
    thead{
      width: 100%;
      background-color: #e8e9ed;
      tr{
        th{
          position: relative;
          text-align: center;
          vertical-align: middle;
          padding: 6px;
          // input[type=checkbox]{
          //   width: 25px;
          //   height: 25px;
          //   vertical-align: middle;
          // }
          div:nth-of-type(1){
            display: inline-block;
          }
          div:nth-of-type(2){
            cursor: pointer;
            position: absolute;
            top: 50%;
            transform: translateY(-50%);
            display: inline-block;
            margin-left: 3px;
            .sort{
              display: block;
              width: 0px;
              height: 0px;
              border-width: 7px;
              border-style:solid;
            }
            .up{
              border-color: transparent transparent #868893 transparent;
              margin-bottom: 8px;
            }
            .down{
              border-color: #868893 transparent transparent transparent;
            }
            .top{
              border-color: transparent transparent #eb6767 transparent;
            }
            .bottom{
              border-color: #eb6767 transparent transparent transparent;
            }
          }
          .colFilter{
            display: inline-block;
            margin-left: 10px;
            border: 1px solid #d2d6de;
          }
        }
        .bordered{
          border: 1px solid #ccc;
        }
        .marginL{
          margin-left: -10px;
        }
      }
    }
    tbody{
      width: 100%;
      background-color: #FFFFFF;
      tr{
        width: 100%;
        border: 1px solid #e4e4e4;
        cursor: pointer;
        transition: all .3s ease;
        td{
          text-align: center;
          vertical-align: middle;
          padding: 6px;
          // input[type=checkbox]{
          //   width: 25px;
          //   height: 25px;
          //   vertical-align: middle;
          // }
        }
      }
      tr:hover{
        background-color: #F9F9F9;
      }
    }
  }}複製程式碼


四、使用Table元件

以下是在render函式中的程式碼


以下是傳入的columns的程式碼(關鍵)


注意:如需新增表頭新增子級表頭,只需在columns陣列中的其中一項新增children欄位,children為一個陣列


相關文章