diff --git a/examples/react-dnd.html b/examples/react-dnd.html new file mode 100644 index 000000000..e69de29bb diff --git a/examples/react-dnd.js b/examples/react-dnd.js new file mode 100644 index 000000000..886b6f0b0 --- /dev/null +++ b/examples/react-dnd.js @@ -0,0 +1,187 @@ +/* eslint-disable no-unused-expressions,new-cap */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { injectGlobal } from 'styled-components'; +import update from 'immutability-helper'; +import { DragDropContext, DragSource, DropTarget } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; +import Table from 'rc-table'; +import 'rc-table/assets/index.less'; + +injectGlobal` + tr.drop-over-downward td { + border-bottom: 2px dashed red; + } + + tr.drop-over-upward td { + border-top: 2px dashed red; + } +`; + +function dragDirection( + dragIndex, + hoverIndex, + initialClientOffset, + clientOffset, + sourceClientOffset, +) { + const hoverMiddleY = (initialClientOffset.y - sourceClientOffset.y) / 2; + const hoverClientY = clientOffset.y - sourceClientOffset.y; + if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) { + return 'downward'; + } + if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) { + return 'upward'; + } +} + +let BodyRow = (props) => { + const { + isOver, + connectDragSource, + connectDropTarget, + moveRow, + dragRow, + clientOffset, + sourceClientOffset, + initialClientOffset, + ...restProps, + } = props; + const style = { cursor: 'move' }; + + let className = restProps.className; + if (isOver && initialClientOffset) { + const direction = dragDirection( + dragRow.index, + restProps.index, + initialClientOffset, + clientOffset, + sourceClientOffset + ); + if (direction === 'downward') { + className += ' drop-over-downward'; + } + if (direction === 'upward') { + className += ' drop-over-upward'; + } + } + + return connectDragSource( + connectDropTarget( + + ) + ); +}; + +const rowSource = { + beginDrag(props) { + return { + index: props.index, + }; + }, +}; + +const rowTarget = { + drop(props, monitor) { + const dragIndex = monitor.getItem().index; + const hoverIndex = props.index; + + // Don't replace items with themselves + if (dragIndex === hoverIndex) { + return; + } + + // Time to actually perform the action + props.moveRow(dragIndex, hoverIndex); + + // Note: we're mutating the monitor item here! + // Generally it's better to avoid mutations, + // but it's good here for the sake of performance + // to avoid expensive index searches. + monitor.getItem().index = hoverIndex; + }, +}; + +BodyRow = DropTarget('row', rowTarget, (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + sourceClientOffset: monitor.getSourceClientOffset(), +}))( + DragSource('row', rowSource, (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + dragRow: monitor.getItem(), + clientOffset: monitor.getClientOffset(), + initialClientOffset: monitor.getInitialClientOffset(), + }))(BodyRow) +); + +const columns = [ + { title: 'title1', dataIndex: 'a', key: 'a', width: 100 }, + { id: '123', title: 'title2', dataIndex: 'b', key: 'b', width: 100 }, + { title: 'title3', dataIndex: 'c', key: 'c', width: 200 }, + { + title: 'Operations', + dataIndex: '', + key: 'd', + render() { + return Operations; + }, + }, +]; + +class Demo extends React.Component { + state = { + data: [ + { a: '123', key: '1' }, + { a: 'cdd', b: 'edd', key: '2' }, + { a: '1333', c: 'eee', d: 2, key: '3' }, + ], + } + + components = { + body: { + row: BodyRow, + }, + } + + moveRow = (dragIndex, hoverIndex) => { + const { data } = this.state; + const dragRow = data[dragIndex]; + + this.setState( + update(this.state, { + data: { + $splice: [[dragIndex, 1], [hoverIndex, 0, dragRow]], + }, + }), + ); + } + + render() { + return ( + ({ + index, + moveRow: this.moveRow, + })} + /> + ); + } +} + +Demo = DragDropContext(HTML5Backend)(Demo); + +ReactDOM.render( +
+

Integrate with react-dnd

+ +
, + document.getElementById('__react-content') +); diff --git a/examples/styled-components.html b/examples/styled-components.html new file mode 100644 index 000000000..e69de29bb diff --git a/examples/styled-components.js b/examples/styled-components.js new file mode 100644 index 000000000..fa453e009 --- /dev/null +++ b/examples/styled-components.js @@ -0,0 +1,45 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import styled from 'styled-components'; +import Table from 'rc-table'; +import 'rc-table/assets/index.less'; + +const columns = [ + { title: 'title1', dataIndex: 'a', key: 'a', width: 100 }, + { id: '123', title: 'title2', dataIndex: 'b', key: 'b', width: 100 }, + { title: 'title3', dataIndex: 'c', key: 'c', width: 200 }, + { + title: 'Operations', + dataIndex: '', + key: 'd', + render() { + return Operations; + }, + }, +]; + +const data = [ + { a: '123', key: '1' }, + { a: 'cdd', b: 'edd', key: '2' }, + { a: '1333', c: 'eee', d: 2, key: '3' }, +]; + +const BodyRow = styled.tr` + &:hover { + background: palevioletred !important; + } +`; + +const components = { + body: { + row: BodyRow, + }, +}; + +ReactDOM.render( +
+

Integrate with styled-components

+
+ , + document.getElementById('__react-content') +); diff --git a/package.json b/package.json index e75884906..07a2df6b1 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "babel-runtime": "6.x", "component-classes": "^1.2.6", "lodash.get": "^4.4.2", + "lodash.merge": "^4.6.0", "mini-store": "^1.0.2", "prop-types": "^15.5.8", "rc-util": "^4.0.4", @@ -82,6 +83,7 @@ "enzyme": "^3.1.0", "enzyme-adapter-react-16": "^1.0.1", "enzyme-to-json": "^3.1.2", + "immutability-helper": "^2.4.0", "jest": "^21.2.1", "pre-commit": "1.x", "rc-animate": "^2.3.0", @@ -89,8 +91,12 @@ "rc-menu": "^5.0.11", "rc-tools": "7.x", "react": "^16.0.0", + "react-dnd": "^2.5.4", + "react-dnd-html5-backend": "^2.5.4", "react-dom": "^16.0.0", - "react-test-renderer": "^16.0.0" + "react-test-renderer": "^16.0.0", + "react-virtualized": "^9.12.0", + "styled-components": "^2.2.1" }, "pre-commit": [ "lint" diff --git a/src/BaseTable.js b/src/BaseTable.js index 91d3cc867..f4b7c59da 100644 --- a/src/BaseTable.js +++ b/src/BaseTable.js @@ -33,7 +33,7 @@ class BaseTable extends React.Component { renderRows = (renderData, indent, ancestorKeys = []) => { const { table } = this.context; - const { columnManager } = table; + const { columnManager, components } = table; const { prefixCls, childrenColumnName, @@ -44,6 +44,7 @@ class BaseTable extends React.Component { onRowContextMenu, onRowMouseEnter, onRowMouseLeave, + onRow, } = table.props; const { getRowKey, fixed, expander } = this.props; @@ -95,6 +96,7 @@ class BaseTable extends React.Component { prefixCls={rowPrefixCls} childrenColumnName={childrenColumnName} columns={leafColumns} + onRow={onRow} onRowDoubleClick={onRowDoubleClick} onRowContextMenu={onRowContextMenu} onRowMouseEnter={onRowMouseEnter} @@ -103,6 +105,7 @@ class BaseTable extends React.Component { rowKey={key} ancestorKeys={ancestorKeys} ref={rowRef(record, i, indent)} + components={components} {...expandableRow} /> )} @@ -129,7 +132,9 @@ class BaseTable extends React.Component { } render() { - const { prefixCls, scroll, data, getBodyWrapper } = this.context.table.props; + const { table } = this.context; + const { components } = table; + const { prefixCls, scroll, data, getBodyWrapper } = table.props; const { expander, tableClassName, hasHead, hasBody, fixed, columns } = this.props; const tableStyle = {}; @@ -142,16 +147,19 @@ class BaseTable extends React.Component { } } + const Table = hasBody ? components.table : 'table'; + const BodyWrapper = components.body.wrapper; + return ( -
+
{hasHead && } {hasBody && getBodyWrapper( - + {this.renderRows(data, 0)} - + )} -
+ ); } } diff --git a/src/ExpandableTable.js b/src/ExpandableTable.js index 797b49817..9656def33 100644 --- a/src/ExpandableTable.js +++ b/src/ExpandableTable.js @@ -111,12 +111,14 @@ class ExpandableTable extends React.Component { return; } - rows[0].unshift({ - key: 'rc-table-expandIconAsCell', + const iconColumn = { + key: 'rc-table-expand-icon-cell', className: `${prefixCls}-expand-icon-th`, title: '', rowSpan: rows.length, - }); + }; + + rows[0].unshift({ ...iconColumn, column: iconColumn }); } renderExpandedRow(content, className, ancestorKeys, fixed) { @@ -146,6 +148,12 @@ class ExpandableTable extends React.Component { } const rowKey = `${ancestorKeys[0]}-extra-row`; + const components = { + body: { + row: 'tr', + cell: 'td', + }, + }; return ( ); } diff --git a/src/Table.js b/src/Table.js index 36f5700a6..0a511a827 100644 --- a/src/Table.js +++ b/src/Table.js @@ -4,6 +4,7 @@ import { debounce, warningOnce } from './utils'; import shallowequal from 'shallowequal'; import addEventListener from 'rc-util/lib/Dom/addEventListener'; import { Provider, create } from 'mini-store'; +import merge from 'lodash.merge'; import ColumnManager from './ColumnManager'; import classes from 'component-classes'; import HeadTable from './HeadTable'; @@ -20,6 +21,8 @@ export default class Table extends React.Component { style: PropTypes.object, rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), rowClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + onRow: PropTypes.func, + onHeaderRow: PropTypes.func, onRowClick: PropTypes.func, onRowDoubleClick: PropTypes.func, onRowContextMenu: PropTypes.func, @@ -33,11 +36,25 @@ export default class Table extends React.Component { rowRef: PropTypes.func, getBodyWrapper: PropTypes.func, children: PropTypes.node, + components: PropTypes.shape({ + table: PropTypes.any, + header: PropTypes.shape({ + wrapper: PropTypes.any, + row: PropTypes.any, + cell: PropTypes.any, + }), + body: PropTypes.shape({ + wrapper: PropTypes.any, + row: PropTypes.any, + cell: PropTypes.any, + }), + }), ...ExpandableTable.PropTypes, } static childContextTypes = { table: PropTypes.any, + components: PropTypes.any, } static defaultProps = { @@ -45,6 +62,8 @@ export default class Table extends React.Component { useFixedHeader: false, rowKey: 'key', rowClassName: () => '', + onRow() {}, + onHeaderRow() {}, onRowClick() {}, onRowDoubleClick() {}, onRowContextMenu() {}, @@ -82,6 +101,19 @@ export default class Table extends React.Component { props: this.props, columnManager: this.columnManager, saveRef: this.saveRef, + components: merge({ + table: 'table', + header: { + wrapper: 'thead', + row: 'tr', + cell: 'th', + }, + body: { + wrapper: 'tbody', + row: 'tr', + cell: 'td', + }, + }, this.props.components), }, }; } diff --git a/src/TableCell.js b/src/TableCell.js index 797e1e5e5..2619da3da 100644 --- a/src/TableCell.js +++ b/src/TableCell.js @@ -11,6 +11,7 @@ export default class TableCell extends React.Component { indentSize: PropTypes.number, column: PropTypes.object, expandIcon: PropTypes.node, + component: PropTypes.any, } isInvalidRenderCellText(text) { @@ -26,8 +27,16 @@ export default class TableCell extends React.Component { } render() { - const { record, indentSize, prefixCls, indent, - index, expandIcon, column } = this.props; + const { + record, + indentSize, + prefixCls, + indent, + index, + expandIcon, + column, + component: BodyCell, + } = this.props; const { dataIndex, render, className = '' } = column; // We should return undefined if no dataIndex is specified, but in order to @@ -54,6 +63,10 @@ export default class TableCell extends React.Component { } } + if (column.onCell) { + tdProps = { ...tdProps, ...column.onCell(record) }; + } + // Fix https://github.com/ant-design/ant-design/issues/1202 if (this.isInvalidRenderCellText(text)) { text = null; @@ -70,15 +83,15 @@ export default class TableCell extends React.Component { return null; } return ( - {indentText} {expandIcon} {text} - + ); } } diff --git a/src/TableHeader.js b/src/TableHeader.js index 51f124fcf..5cfcff8f2 100644 --- a/src/TableHeader.js +++ b/src/TableHeader.js @@ -16,6 +16,7 @@ function getHeaderRows(columns, currentRow = 0, rows) { key: column.key, className: column.className || '', children: column.title, + column, }; if (column.children) { getHeaderRows(column.children, currentRow + 1, rows); @@ -34,7 +35,8 @@ function getHeaderRows(columns, currentRow = 0, rows) { } export default function TableHeader(props, { table }) { - const { prefixCls, showHeader } = table.props; + const { components } = table; + const { prefixCls, showHeader, onHeaderRow } = table.props; const { expander, columns, fixed } = props; if (!showHeader) { @@ -45,20 +47,25 @@ export default function TableHeader(props, { table }) { expander.renderExpandIndentCell(rows, fixed); + const HeaderWrapper = components.header.wrapper; + return ( - + { rows.map((row, index) => ( )) } - + ); } @@ -66,6 +73,7 @@ TableHeader.propTypes = { fixed: PropTypes.string, columns: PropTypes.array.isRequired, expander: PropTypes.object.isRequired, + onHeaderRow: PropTypes.func, }; TableHeader.contextTypes = { diff --git a/src/TableHeaderRow.js b/src/TableHeaderRow.js index 6b2301684..bbf98c7cc 100644 --- a/src/TableHeaderRow.js +++ b/src/TableHeaderRow.js @@ -1,13 +1,21 @@ import React from 'react'; import { connect } from 'mini-store'; -function TableHeaderRow({ row, height }) { - const style = { height }; +function TableHeaderRow({ row, index, height, components, onHeaderRow }) { + const HeaderRow = components.header.row; + const HeaderCell = components.header.cell; + const rowProps = onHeaderRow(row.map(cell => cell.column), index); + const customStyle = rowProps ? rowProps.style : {}; + const style = { height, ...customStyle }; return ( - - {row.map((cell, i) => )} - + + {row.map((cell, i) => { + const { column, ...cellProps } = cell; + const customProps = column.onHeaderCell ? column.onHeaderCell(column) : {}; + return ; + })} + ); } diff --git a/src/TableRow.js b/src/TableRow.js index bbb93bb42..a767a3f4f 100644 --- a/src/TableRow.js +++ b/src/TableRow.js @@ -1,10 +1,12 @@ import React from 'react'; +import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { connect } from 'mini-store'; import TableCell from './TableCell'; class TableRow extends React.Component { static propTypes = { + onRow: PropTypes.func, onRowClick: PropTypes.func, onRowDoubleClick: PropTypes.func, onRowContextMenu: PropTypes.func, @@ -36,9 +38,11 @@ class TableRow extends React.Component { ]), renderExpandIcon: PropTypes.func, renderExpandIconCell: PropTypes.func, + components: PropTypes.any, } static defaultProps = { + onRow() {}, onRowClick() {}, onRowDoubleClick() {}, onRowContextMenu() {}, @@ -56,9 +60,12 @@ class TableRow extends React.Component { super(props); this.shouldRender = props.visible; + } - // avoid creating new object which may fail the sCU. - this.style = {}; + componentDidMount() { + if (this.shouldRender) { + this.saveRowRef(); + } } componentWillReceiveProps(nextProps) { @@ -67,6 +74,12 @@ class TableRow extends React.Component { } } + componentDidUpdate() { + if (this.shouldRender && !this.rowRef) { + this.saveRowRef(); + } + } + onRowClick = (event) => { const { record, index, onRowClick } = this.props; onRowClick(record, index, event); @@ -116,12 +129,11 @@ class TableRow extends React.Component { return this.style; } - saveRowRef = (node) => { - this.rowRef = node; - if (node) { - if (!this.props.fixed) { - this.setHeight(); - } + saveRowRef() { + this.rowRef = ReactDOM.findDOMNode(this); + + if (!this.props.fixed) { + this.setHeight(); } } @@ -135,14 +147,21 @@ class TableRow extends React.Component { columns, record, index, + onRow, indent, indentSize, hovered, + height, + visible, + components, hasExpandIcon, renderExpandIcon, renderExpandIconCell, } = this.props; + const BodyRow = components.body.row; + const BodyCell = components.body.cell; + let { className } = this.props; if (hovered) { @@ -164,6 +183,7 @@ class TableRow extends React.Component { column={columns[i]} key={columns[i].key || columns[i].dataIndex} expandIcon={hasExpandIcon(i) && renderExpandIcon()} + component={BodyCell} /> ); } @@ -171,19 +191,29 @@ class TableRow extends React.Component { const rowClassName = `${prefixCls} ${className} ${prefixCls}-level-${indent}`.trim(); + const rowProps = onRow(record, index); + const customStyle = rowProps ? rowProps.style : {}; + let style = { height }; + + if (!visible) { + style.display = 'none'; + } + + style = { ...style, ...customStyle }; + return ( - {cells} - + ); } } diff --git a/tests/Table.spec.js b/tests/Table.spec.js index 22aab3717..16fde87f5 100644 --- a/tests/Table.spec.js +++ b/tests/Table.spec.js @@ -440,4 +440,100 @@ describe('Table', () => { })); expect(wrapper).toMatchSnapshot(); }); + + it('renders onRow correctly', () => { + const onRow = (record, index) => ({ + id: `row-${record.key}`, + index, + }); + const wrapper = render(createTable({ onRow })); + + expect(wrapper.find('tbody tr')).toMatchSnapshot(); + }); + + it('renders column.onCell correctly', () => { + const onCell = (record) => ({ + id: `cell-${record.name}`, + }); + const columns = [ + { title: 'Name', dataIndex: 'name', key: 'name', onCell }, + ]; + const wrapper = render(createTable({ columns })); + + expect(wrapper.find('tbody td')).toMatchSnapshot(); + }); + + it('renders onHeaderRow correctly', () => { + const onHeaderRow = jest.fn((columns, index) => ({ + id: `header-row-${index}`, + })); + const wrapper = render(createTable({ onHeaderRow })); + + expect(wrapper.find('thead tr')).toMatchSnapshot(); + expect(onHeaderRow).toBeCalledWith( + [{ title: 'Name', dataIndex: 'name', key: 'name' }], + 0, + ); + }); + + it('renders column.onHeaderCell', () => { + const onHeaderCell = (column) => ({ + id: `header-cell-${column.key}`, + }); + const columns = [ + { title: 'Name', dataIndex: 'name', key: 'name', onHeaderCell }, + ]; + const wrapper = render(createTable({ columns })); + + expect(wrapper.find('thead th')).toMatchSnapshot(); + }); + + describe('custom components', () => { + const MyTable = (props) => ; + const HeaderWrapper = (props) => ; + const HeaderRow = (props) => ; + const HeaderCell = (props) => ; + const BodyRow = (props) => ; + const BodyCell = (props) => , + , +] +`; + +exports[`Table renders column.onHeaderCell 1`] = ` + +`; + exports[`Table renders correctly 1`] = `
`; +exports[`Table renders onHeaderRow correctly 1`] = ` +
+ + +`; + +exports[`Table renders onRow correctly 1`] = ` +Array [ + + + , + + + , +] +`; + exports[`Table renders rowSpan correctly 1`] = `
; + const BodyWrapper = (props) =>
; + + const components = { + table: MyTable, + header: { + wrapper: HeaderWrapper, + row: HeaderRow, + cell: HeaderCell, + }, + body: { + wrapper: BodyWrapper, + row: BodyRow, + cell: BodyCell, + }, + }; + + it('renders correctly', () => { + const wrapper = render(createTable({ components })); + + expect(wrapper).toMatchSnapshot(); + }); + + it('renders fixed column and header correctly', () => { + const columns = [ + { title: 'Name', dataIndex: 'name', key: 'name', fixed: 'left' }, + { title: 'Age', dataIndex: 'age', key: 'age' }, + { title: 'Gender', dataIndex: 'gender', key: 'gender', fixed: 'right' }, + ]; + const sampleData = [ + { key: 0, name: 'Lucy', age: 27, gender: 'F' }, + ]; + const wrapper = render(createTable({ + columns, + data: sampleData, + components, + scroll: { y: 100 }, + })); + + expect(wrapper).toMatchSnapshot(); + }); + }); }); diff --git a/tests/__snapshots__/Table.spec.js.snap b/tests/__snapshots__/Table.spec.js.snap index 98e9c233f..c9e320534 100644 --- a/tests/__snapshots__/Table.spec.js.snap +++ b/tests/__snapshots__/Table.spec.js.snap @@ -1,5 +1,312 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Table custom components renders correctly 1`] = ` +
+
+
+ + + + + + + + + + + + + + + + + +
+ Name +
+ + Lucy +
+ + Jack +
+
+
+
+`; + +exports[`Table custom components renders fixed column and header correctly 1`] = ` +
+
+
+
+ + + + + + + + + + + + + +
+ Name + + Age + + Gender +
+
+
+ + + + + + + + + + + + + +
+ + Lucy + + 27 + + F +
+
+
+
+
+ + + + + + + + + +
+ Name +
+
+
+
+ + + + + + + + + +
+ + Lucy +
+
+
+
+
+
+ + + + + + + + + +
+ Gender +
+
+
+
+ + + + + + + + + +
+ F +
+
+
+
+
+
+`; + exports[`Table dataIndex render text by path 1`] = `
`; +exports[`Table renders column.onCell correctly 1`] = ` +Array [ +
+ + Lucy + + + Jack + + Name +
+ Name +
+ + Lucy +
+ + Jack +