簡述用React實現Table組件

1、背景

因爲剛到如今就任的公司接手到公司後臺的項目代碼的時候,發現系統中大量使用了表格,由於代碼經多人蔘與過,致使有些代碼塊是經過ul>li標籤佈局有些地方則是經過div佈局等等,致使了代碼的嚴重冗餘;簡單的表格還好,若是是表格中須要對某些字段進行排序的話,又會大大增長代碼的量級和可讀性,如圖javascript


代碼爲:java


從上圖部分截圖能夠看出,一個表格的代碼量大概能佔到200行代碼,並且每一個字段的排序的上下箭頭都會增長兩個state去進行管理,5個字段須要排序則須要增長10個state,也就意味着每次咱們在進入路由組件的時候和進行各類刷新重置操做的時候都須要去處理這10個state等等,還有一個問題就是表格的佈局,每一個字段所佔的寬和高度都是class去設置的,也就意味着,每次產品來告訴你,這個表格須要增長一個字段,你也就得從新去計算全部的字段所佔寬度和分配適合的寬度。以上所述的問題違背了程序開發的耦合性和複用性,因此決定封裝一個Table組件去解決這個問題。react

2、分析須要實現Table的API

一、傳入一個數組自動構建出表頭以及該表頭下這一列的顯示的內容,命名爲columns,數據類型爲array
二、傳入表格須要顯示的數據源數組,命名爲dataSource,數據類型爲array
三、缺省狀態下,表格須要展現的內容,命名爲emptyText,數據類型爲string
四、傳入表格的分頁項,包含當前頁碼,總頁碼,以及分頁器的點擊事件,命名爲pagination,數據類型爲object
五、傳入一個表格每行勾選的配置對象,包含是否使用勾選、勾選的點擊事件,便於知足對錶格勾選項進行批量處理的需求,命名爲rowSelection,數據類型爲object
六、傳入一個表格行的點擊事件,便於知足點擊當前行進入詳情的需求,命名爲onRowClick,數據類型爲function
七、傳入表格的樣式對象,給表格內部的外層包裹容器添加行內樣式,命名爲style,數據類型爲object
八、傳入一個className給表格內部的外層包裹容器添加className,能夠實如今表格組件外部設置表格的每一列的className,數據類型爲string數組

3、具體實現

一、總體代碼app

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>
    );
  }}複製代碼

二、表頭部分(thead)less

//遞歸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進行表頭的渲染函數

三、表體部分(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>);
          }
        }
      })
    );
   } 複製代碼


四、表格less部分ui

.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;
      }
    }
  }}複製代碼


4、使用Table組件

如下是在render函數中的代碼this


如下是傳入的columns的代碼(關鍵)


注意:如需添加表頭添加子級表頭,只需在columns數組中的其中一項添加children字段,children爲一個數組

相關文章
相關標籤/搜索