diff --git a/assets/index.less b/assets/index.less index 173d5f1eb..c8d39e65b 100644 --- a/assets/index.less +++ b/assets/index.less @@ -38,7 +38,8 @@ overflow: scroll; } - &-fixed-header &-scroll &-header { + &-fixed-header &-scroll &-header, + &-fixed-header &-scroll &-in-table-footer { overflow-x: scroll; padding-bottom: 20px; margin-bottom: -20px; diff --git a/examples/columnFooter.html b/examples/columnFooter.html new file mode 100644 index 000000000..e69de29bb diff --git a/examples/columnFooter.js b/examples/columnFooter.js new file mode 100644 index 000000000..c23d5701b --- /dev/null +++ b/examples/columnFooter.js @@ -0,0 +1,64 @@ +/* eslint-disable no-console,func-names,react/no-multi-comp */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Table from 'rc-table'; +import 'rc-table/assets/index.less'; + +const columns = [ + { + title: 'Name', + dataIndex: 'name', + width: 100, + fixed: 'left', + footer: 'Summary', + }, + { + title: 'Money', + dataIndex: 'money', + width: 100, + render: text => `$${text.toFixed(2)}`, + footer: data => `Total: $${data.reduce((acc, row) => acc + row.money, 0).toFixed(2)}`, + }, + { title: 'Address', dataIndex: 'address', width: 300 }, +]; + +const data = [ + { + name: 'John Brown', + money: 300, + address: 'New York No. 1 Lake Park', + }, + { + name: 'Jim Green', + money: 128, + address: 'London No. 1 Lake Park', + }, + { + name: 'Joe Black', + money: 240, + address: 'Sidney No. 1 Lake Park', + }, + { + name: 'Mick Sydney', + money: 300, + address: 'Sidney No. 1 Lake Park', + }, + { + name: 'Miguel Manning', + money: 120, + address: 'Sidney No. 1 Lake Park', + }, + { + name: 'John Appleseed', + money: 256, + address: '1 Infinite Loop; Cupertino, CA 95014', + }, +]; + +ReactDOM.render( +
+

Demonstrate column footer

+ + , + document.getElementById('__react-content') +); diff --git a/src/BaseTable.js b/src/BaseTable.js index 18d91610f..0d1be2418 100644 --- a/src/BaseTable.js +++ b/src/BaseTable.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { connect } from 'mini-store'; import ColGroup from './ColGroup'; import TableHeader from './TableHeader'; +import TableFooter from './TableFooter'; import TableRow from './TableRow'; import ExpandableRow from './ExpandableRow'; @@ -16,6 +17,7 @@ class BaseTable extends React.Component { tableClassName: PropTypes.string.isRequired, hasHead: PropTypes.bool.isRequired, hasBody: PropTypes.bool.isRequired, + hasFoot: PropTypes.bool.isRequired, store: PropTypes.object.isRequired, expander: PropTypes.object.isRequired, getRowKey: PropTypes.func, @@ -134,7 +136,7 @@ class BaseTable extends React.Component { const { table } = this.context; const { components } = table; const { prefixCls, scroll, data, getBodyWrapper } = table.props; - const { expander, tableClassName, hasHead, hasBody, fixed, columns } = this.props; + const { expander, tableClassName, hasHead, hasBody, hasFoot, fixed, columns } = this.props; const tableStyle = {}; if (!fixed && scroll.x) { @@ -165,6 +167,7 @@ class BaseTable extends React.Component {
{hasHead && } + {hasFoot && } {body}
); diff --git a/src/BodyTable.js b/src/BodyTable.js index 3648dafb5..bc3f806cc 100644 --- a/src/BodyTable.js +++ b/src/BodyTable.js @@ -51,6 +51,7 @@ export default function BodyTable(props, { table }) { tableClassName={tableClassName} hasHead={!useFixedHeader} hasBody + hasFoot={!useFixedHeader} fixed={fixed} columns={columns} expander={expander} diff --git a/src/Column.js b/src/Column.js index f891975b1..8b4d898ae 100644 --- a/src/Column.js +++ b/src/Column.js @@ -6,6 +6,7 @@ export default class Column extends Component { className: PropTypes.string, colSpan: PropTypes.number, title: PropTypes.node, + footer: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), dataIndex: PropTypes.string, width: PropTypes.oneOfType([ PropTypes.number, diff --git a/src/FootTable.js b/src/FootTable.js new file mode 100644 index 000000000..c20549a1d --- /dev/null +++ b/src/FootTable.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { measureScrollbar } from './utils'; +import BaseTable from './BaseTable'; + +export default function FootTable(props, { table }) { + const { prefixCls, scroll } = table.props; + const { columns, fixed, tableClassName, handleBodyScrollLeft, expander } = props; + const { saveRef } = table; + let { useFixedHeader } = table.props; + const footStyle = {}; + + if (scroll.y) { + useFixedHeader = true; + // Add negative margin bottom for scroll bar overflow bug + const scrollbarWidth = measureScrollbar(); + if (scrollbarWidth > 0 && !fixed) { + footStyle.marginBottom = `-${scrollbarWidth}px`; + footStyle.paddingBottom = '0px'; + } + } + + if (!useFixedHeader) { + return null; + } + + return ( +
+ +
+ ); +} + +FootTable.propTypes = { + fixed: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + ]), + columns: PropTypes.array.isRequired, + tableClassName: PropTypes.string.isRequired, + handleBodyScrollLeft: PropTypes.func.isRequired, + expander: PropTypes.object.isRequired, +}; + +FootTable.contextTypes = { + table: PropTypes.any, +}; diff --git a/src/HeadTable.js b/src/HeadTable.js index 5b427f9e6..6c206aff5 100644 --- a/src/HeadTable.js +++ b/src/HeadTable.js @@ -36,6 +36,7 @@ export default function HeadTable(props, { table }) { tableClassName={tableClassName} hasHead hasBody={false} + hasFoot={false} fixed={fixed} columns={columns} expander={expander} diff --git a/src/Table.js b/src/Table.js index 8d9ecea1b..3c5350e95 100644 --- a/src/Table.js +++ b/src/Table.js @@ -9,6 +9,7 @@ import ColumnManager from './ColumnManager'; import classes from 'component-classes'; import HeadTable from './HeadTable'; import BodyTable from './BodyTable'; +import FootTable from './FootTable'; import ExpandableTable from './ExpandableTable'; export default class Table extends React.Component { @@ -49,6 +50,11 @@ export default class Table extends React.Component { row: PropTypes.any, cell: PropTypes.any, }), + footer: PropTypes.shape({ + wrapper: PropTypes.any, + row: PropTypes.any, + cell: PropTypes.any, + }), }), ...ExpandableTable.PropTypes, } @@ -126,6 +132,11 @@ export default class Table extends React.Component { row: 'tr', cell: 'td', }, + footer: { + wrapper: 'tfoot', + row: 'tr', + cell: 'td', + }, }, this.props.components), }, }; @@ -260,6 +271,9 @@ export default class Table extends React.Component { if (this.bodyTable) { this.bodyTable.scrollLeft = 0; } + if (this.footTable) { + this.footTable.scrollLeft = 0; + } } hasScrollX() { @@ -274,12 +288,17 @@ export default class Table extends React.Component { } const target = e.target; const { scroll = {} } = this.props; - const { headTable, bodyTable } = this; + const { headTable, bodyTable, footTable } = this; if (target.scrollLeft !== this.lastScrollLeft && scroll.x) { - if (target === bodyTable && headTable) { - headTable.scrollLeft = target.scrollLeft; - } else if (target === headTable && bodyTable) { - bodyTable.scrollLeft = target.scrollLeft; + if (target === bodyTable) { + if (headTable) headTable.scrollLeft = target.scrollLeft; + if (footTable) footTable.scrollLeft = target.scrollLeft; + } else if (target === headTable) { + if (bodyTable) bodyTable.scrollLeft = target.scrollLeft; + if (footTable) footTable.scrollLeft = target.scrollLeft; + } else if (target === footTable) { + if (bodyTable) bodyTable.scrollLeft = target.scrollLeft; + if (headTable) headTable.scrollLeft = target.scrollLeft; } this.setScrollPositionClassName(); } @@ -390,7 +409,18 @@ export default class Table extends React.Component { /> ); - return [headTable, bodyTable]; + const footTable = ( + + ); + + return [headTable, bodyTable, footTable]; } renderTitle() { diff --git a/src/TableFooter.js b/src/TableFooter.js new file mode 100644 index 000000000..65fb8c347 --- /dev/null +++ b/src/TableFooter.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function getColumnFooter(col) { + if (typeof col.footer === 'function') return col.footer; + return () => col.footer; +} + +export default function TableFooter(props, { table }) { + const { columnManager, components } = table; + const { prefixCls, data, expandIconAsCell } = table.props; + const { fixed } = props; + + if (!columnManager.leafColumns().some(col => col.footer)) { + return null; + } + + const expandIconCol = { + key: 'expand-icon-placeholder', + render: () => null, + }; + + let leafColumns; + if (fixed === 'left') { + leafColumns = columnManager.leftLeafColumns(); + if (expandIconAsCell) { + leafColumns = [expandIconCol, ...leafColumns]; + } + } else if (fixed === 'right') { + leafColumns = columnManager.rightLeafColumns(); + } else { + leafColumns = columnManager.leafColumns(); + if (expandIconAsCell) { + leafColumns = [expandIconCol, ...leafColumns]; + } + } + + const FooterWrapper = components.footer.wrapper; + const FooterRow = components.footer.row; + const FooterCell = components.footer.cell; + + return ( + + + {leafColumns.map(col => + + {col.footer === undefined ? null : getColumnFooter(col)(data)} + + )} + + + ); +} + +TableFooter.propTypes = { + fixed: PropTypes.string, + columns: PropTypes.array.isRequired, +}; + +TableFooter.contextTypes = { + table: PropTypes.any, +};