Optimize table selection (#3757)

* Extract SelectionRadio, SelectionCheckbox, SelectionCheckboxAll

* Add some tests
This commit is contained in:
Wei Zhu 2016-11-11 15:26:51 +08:00 committed by 偏右
parent be9ed0388c
commit 0e6ac6bc4c
6 changed files with 475 additions and 112 deletions

View File

@ -0,0 +1,81 @@
import React from 'react';
import Checkbox from '../checkbox';
import Radio from '../radio';
import { Store } from './createStore';
export interface SelectionBoxProps {
store: Store;
type: string;
defaultSelection: string[];
rowIndex: string;
disabled?: boolean;
onChange: (e) => void;
}
export default class SelectionBox extends React.Component<SelectionBoxProps, any> {
unsubscribe: () => void;
constructor(props) {
super(props);
this.state = {
checked: this.getCheckState(props),
};
}
componentDidMount() {
this.subscribe();
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
subscribe() {
const { store } = this.props;
this.unsubscribe = store.subscribe(() => {
const checked = this.getCheckState(this.props);
if (checked !== this.state.checked) {
this.setState({ checked });
}
});
}
getCheckState(props) {
const { store, defaultSelection, rowIndex } = props;
let checked = false;
if (store.getState().selectionDirty) {
checked = store.getState().selectedRowKeys.indexOf(rowIndex) >= 0;
} else {
checked = (store.getState().selectedRowKeys.indexOf(rowIndex) >= 0 ||
defaultSelection.indexOf(rowIndex) >= 0);
}
return checked;
}
render() {
const { type, rowIndex, disabled, onChange } = this.props;
const { checked } = this.state;
if (type === 'radio') {
return (
<Radio
disabled={disabled}
onChange={onChange}
value={rowIndex}
checked={checked}
/>
);
}
return (
<Checkbox
checked={checked}
disabled={disabled}
onChange={onChange}
/>
);
}
}

View File

@ -0,0 +1,114 @@
import React from 'react';
import Checkbox from '../checkbox';
import { Store } from './createStore';
export interface SelectionCheckboxAllProps {
store: Store;
disabled: boolean;
getCheckboxPropsByItem: (item) => any;
getRecordKey: (record, index?) => string;
data: any[];
onChange: (e) => void;
}
export default class SelectionCheckboxAll extends React.Component<SelectionCheckboxAllProps, any> {
unsubscribe: () => void;
constructor(props) {
super(props);
this.state = {
checked: this.getCheckState(),
indeterminate: this.getIndeterminateState(),
};
}
componentDidMount() {
this.subscribe();
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
subscribe() {
const { store } = this.props;
this.unsubscribe = store.subscribe(() => {
const checked = this.getCheckState();
const indeterminate = this.getIndeterminateState();
if (checked !== this.state.checked) {
this.setState({ checked });
}
if (indeterminate !== this.state.indeterminate) {
this.setState({ indeterminate });
}
});
}
checkSelection(data, type, byDefaultChecked) {
const { store, getCheckboxPropsByItem, getRecordKey } = this.props;
// type should be 'every' | 'some'
if (type === 'every' || type === 'some') {
return (
byDefaultChecked
? data[type](item => getCheckboxPropsByItem(item).defaultChecked)
: data[type]((item, i) =>
store.getState().selectedRowKeys.indexOf(getRecordKey(item, i)) >= 0)
);
}
return false;
}
getCheckState() {
const { store, data } = this.props;
let checked;
if (!data.length) {
checked = false;
} else {
checked = store.getState().selectionDirty
? this.checkSelection(data, 'every', false)
: (
this.checkSelection(data, 'every', false) ||
this.checkSelection(data, 'every', true)
);
}
return checked;
}
getIndeterminateState() {
const { store, data } = this.props;
let indeterminate;
if (!data.length) {
indeterminate = false;
} else {
indeterminate = store.getState().selectionDirty
? (
this.checkSelection(data, 'some', false) &&
!this.checkSelection(data, 'every', false)
)
: ((this.checkSelection(data, 'some', false) &&
!this.checkSelection(data, 'every', false)) ||
(this.checkSelection(data, 'some', true) &&
!this.checkSelection(data, 'every', true))
);
}
return indeterminate;
}
render() {
const { disabled, onChange } = this.props;
const { checked, indeterminate } = this.state;
return (
<Checkbox
checked={checked}
indeterminate={indeterminate}
disabled={disabled}
onChange={onChange}
/>
);
}
}

View File

@ -1,7 +1,5 @@
import React from 'react';
import RcTable from 'rc-table';
import Checkbox from '../checkbox';
import Radio from '../radio';
import FilterDropdown from './filterDropdown';
import Pagination, { PaginationProps } from '../pagination';
import Icon from '../icon';
@ -11,6 +9,9 @@ import { flatArray, treeMap } from './util';
import assign from 'object-assign';
import splitObject from '../_util/splitObject';
import warning from '../_util/warning';
import createStore, { Store } from './createStore';
import SelectionBox from './SelectionBox';
import SelectionCheckboxAll from './SelectionCheckboxAll';
function noop() {
}
@ -134,6 +135,7 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
context: TableContext;
CheckboxPropsCache: Object;
store: Store;
constructor(props) {
super(props);
@ -149,9 +151,7 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
this.state = assign({}, this.getSortStateFromColumns(), {
// 减少状态
selectedRowKeys: (props.rowSelection || {}).selectedRowKeys || [],
filters: this.getFiltersFromColumns(),
selectionDirty: false,
pagination: this.hasPagination() ?
assign({}, defaultPagination, pagination, {
current: pagination.defaultCurrent || pagination.current || 1,
@ -160,9 +160,14 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
});
this.CheckboxPropsCache = {};
this.store = createStore({
selectedRowKeys: (props.rowSelection || {}).selectedRowKeys || [],
selectionDirty: false,
});
}
getCheckboxPropsByItem(item) {
getCheckboxPropsByItem = (item) => {
const { rowSelection = {} } = this.props;
if (!rowSelection.getCheckboxProps) {
return {};
@ -204,14 +209,14 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
// dataSource 的变化会清空选中项
if ('dataSource' in nextProps &&
nextProps.dataSource !== this.props.dataSource) {
this.setState({
this.store.setState({
selectionDirty: false,
});
this.CheckboxPropsCache = {};
}
if (nextProps.rowSelection &&
'selectedRowKeys' in nextProps.rowSelection) {
this.setState({
this.store.setState({
selectedRowKeys: nextProps.rowSelection.selectedRowKeys || [],
});
const { rowSelection } = this.props;
@ -246,7 +251,7 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
setSelectedRowKeys(selectedRowKeys, { selectWay, record, checked, changeRowKeys }: any) {
const { rowSelection = {} } = this.props;
if (rowSelection && !('selectedRowKeys' in rowSelection)) {
this.setState({ selectedRowKeys });
this.store.setState({ selectedRowKeys });
}
const data = this.getFlatData();
if (!rowSelection.onChange && !rowSelection[selectWay]) {
@ -385,7 +390,6 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
}
const newState = {
selectionDirty: false,
pagination,
filters: {},
};
@ -409,6 +413,9 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
}
this.setState(newState, () => {
this.store.setState({
selectionDirty: false,
});
const onChange = this.props.onChange;
if (onChange) {
onChange.apply(null, this.prepareParamsArguments(assign({}, this.state, {
@ -422,15 +429,15 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
handleSelect = (record, rowIndex, e) => {
const checked = e.target.checked;
const defaultSelection = this.state.selectionDirty ? [] : this.getDefaultSelection();
let selectedRowKeys = this.state.selectedRowKeys.concat(defaultSelection);
const defaultSelection = this.store.getState().selectionDirty ? [] : this.getDefaultSelection();
let selectedRowKeys = this.store.getState().selectedRowKeys.concat(defaultSelection);
let key = this.getRecordKey(record, rowIndex);
if (checked) {
selectedRowKeys.push(this.getRecordKey(record, rowIndex));
} else {
selectedRowKeys = selectedRowKeys.filter((i) => key !== i);
}
this.setState({
this.store.setState({
selectionDirty: true,
});
this.setSelectedRowKeys(selectedRowKeys, {
@ -442,11 +449,11 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
handleRadioSelect = (record, rowIndex, e) => {
const checked = e.target.checked;
const defaultSelection = this.state.selectionDirty ? [] : this.getDefaultSelection();
let selectedRowKeys = this.state.selectedRowKeys.concat(defaultSelection);
const defaultSelection = this.store.getState().selectionDirty ? [] : this.getDefaultSelection();
let selectedRowKeys = this.store.getState().selectedRowKeys.concat(defaultSelection);
let key = this.getRecordKey(record, rowIndex);
selectedRowKeys = [key];
this.setState({
this.store.setState({
selectionDirty: true,
});
this.setSelectedRowKeys(selectedRowKeys, {
@ -459,8 +466,8 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
handleSelectAllRow = (e) => {
const checked = e.target.checked;
const data = this.getFlatCurrentPageData();
const defaultSelection = this.state.selectionDirty ? [] : this.getDefaultSelection();
const selectedRowKeys = this.state.selectedRowKeys.concat(defaultSelection);
const defaultSelection = this.store.getState().selectionDirty ? [] : this.getDefaultSelection();
const selectedRowKeys = this.store.getState().selectedRowKeys.concat(defaultSelection);
const changableRowKeys = data
.filter(item => !this.getCheckboxPropsByItem(item).disabled)
.map((item, i) => this.getRecordKey(item, i));
@ -482,7 +489,7 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
}
});
}
this.setState({
this.store.setState({
selectionDirty: true,
});
this.setSelectedRowKeys(selectedRowKeys, {
@ -503,7 +510,6 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
pagination.onChange(pagination.current);
const newState = {
selectionDirty: false,
pagination,
};
// Controlled current prop will not respond user interaction
@ -514,6 +520,10 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
}
this.setState(newState);
this.store.setState({
selectionDirty: false,
});
const onChange = this.props.onChange;
if (onChange) {
onChange.apply(null, this.prepareParamsArguments(assign({}, this.state, {
@ -523,48 +533,31 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
}
}
renderSelectionRadio = (_, record, index) => {
let rowIndex = this.getRecordKey(record, index); // 从 1 开始
const props = this.getCheckboxPropsByItem(record);
let checked;
if (this.state.selectionDirty) {
checked = this.state.selectedRowKeys.indexOf(rowIndex) >= 0;
} else {
checked = (this.state.selectedRowKeys.indexOf(rowIndex) >= 0 ||
this.getDefaultSelection().indexOf(rowIndex) >= 0);
}
return (
<span onClick={stopPropagation}>
<Radio disabled={props.disabled}
onChange={(e) => this.handleRadioSelect(record, rowIndex, e)}
value={rowIndex} checked={checked}
/>
</span>
);
renderSelectionBox = (type) => {
return (_, record, index) => {
let rowIndex = this.getRecordKey(record, index); // 从 1 开始
const props = this.getCheckboxPropsByItem(record);
const handleChange = (e) => {
type === 'radio' ? this.handleRadioSelect(record, rowIndex, e) :
this.handleSelect(record, rowIndex, e);
};
return (
<span onClick={stopPropagation}>
<SelectionBox
type={type}
store={this.store}
rowIndex={rowIndex}
disabled={props.disabled}
onChange={handleChange}
defaultSelection={this.getDefaultSelection()}
/>
</span>
);
};
}
renderSelectionCheckBox = (_, record, index) => {
let rowIndex = this.getRecordKey(record, index); // 从 1 开始
let checked;
if (this.state.selectionDirty) {
checked = this.state.selectedRowKeys.indexOf(rowIndex) >= 0;
} else {
checked = (this.state.selectedRowKeys.indexOf(rowIndex) >= 0 ||
this.getDefaultSelection().indexOf(rowIndex) >= 0);
}
const props = this.getCheckboxPropsByItem(record);
return (
<span onClick={stopPropagation}>
<Checkbox
checked={checked}
disabled={props.disabled}
onChange={(e) => this.handleSelect(record, rowIndex, e)}
/>
</span>
);
}
getRecordKey(record, index?): string {
getRecordKey = (record, index?): string => {
const rowKey = this.props.rowKey;
if (typeof rowKey === 'function') {
return rowKey(record, index);
@ -576,19 +569,6 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
return recordKey;
}
checkSelection(data, type, byDefaultChecked) {
// type should be 'every' | 'some'
if (type === 'every' || type === 'some') {
return (
byDefaultChecked
? data[type](item => this.getCheckboxPropsByItem(item).defaultChecked)
: data[type]((item, i) =>
this.state.selectedRowKeys.indexOf(this.getRecordKey(item, i)) >= 0)
);
}
return false;
}
renderRowSelection() {
const { prefixCls, rowSelection } = this.props;
const columns = this.props.columns.concat();
@ -599,51 +579,23 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
}
return true;
});
let checked;
let indeterminate;
if (!data.length) {
checked = false;
indeterminate = false;
} else {
checked = this.state.selectionDirty
? this.checkSelection(data, 'every', false)
: (
this.checkSelection(data, 'every', false) ||
this.checkSelection(data, 'every', true)
);
indeterminate = this.state.selectionDirty
? (
this.checkSelection(data, 'some', false) &&
!this.checkSelection(data, 'every', false)
)
: ((this.checkSelection(data, 'some', false) &&
!this.checkSelection(data, 'every', false)) ||
(this.checkSelection(data, 'some', true) &&
!this.checkSelection(data, 'every', true))
);
}
let selectionColumn;
if (rowSelection.type === 'radio') {
selectionColumn = {
key: 'selection-column',
render: this.renderSelectionRadio,
className: `${prefixCls}-selection-column`,
};
} else {
const selectionColumn: TableColumnConfig<any> = {
key: 'selection-column',
render: this.renderSelectionBox(rowSelection.type),
className: `${prefixCls}-selection-column`,
};
if (rowSelection.type !== 'radio') {
const checkboxAllDisabled = data.every(item => this.getCheckboxPropsByItem(item).disabled);
const checkboxAll = (
<Checkbox checked={checked}
indeterminate={indeterminate}
selectionColumn.title = (
<SelectionCheckboxAll
store={this.store}
data={data}
getCheckboxPropsByItem={this.getCheckboxPropsByItem}
getRecordKey={this.getRecordKey}
disabled={checkboxAllDisabled}
onChange={this.handleSelectAllRow}
/>
);
selectionColumn = {
key: 'selection-column',
title: checkboxAll,
render: this.renderSelectionCheckBox,
className: `${prefixCls}-selection-column`,
};
}
if (columns.some(column => column.fixed === 'left' || column.fixed === true)) {
selectionColumn.fixed = 'left';

View File

@ -0,0 +1,38 @@
import assign from 'object-assign';
export interface Store {
setState: (Object) => void;
getState: () => any;
subscribe: (listener: () => void) => () => void;
}
export default function createStore(initialState): Store {
let state = initialState;
const listeners: any[] = [];
function setState(partial) {
state = assign({}, state, partial);
for (let i = 0; i < listeners.length; i++) {
listeners[i]();
}
}
function getState() {
return state;
}
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
return {
setState,
getState,
subscribe,
};
}

View File

@ -0,0 +1,87 @@
import React from 'react';
import createStore from '../../components/table/createStore';
import SelectionBox from '../../components/table/SelectionBox';
import TestUtils from 'react-addons-test-utils';
describe('SelectionBox', () => {
it('unchecked by selectedRowKeys ', () => {
const store = createStore({
selectedRowKeys: [],
selectionDirty: false,
});
const instance = TestUtils.renderIntoDocument(
<SelectionBox
store={store}
rowIndex="1"
disabled={false}
onChange={() => {}}
defaultSelection={[]}
/>
);
expect(instance.state).toEqual({ checked: false });
});
it('checked by selectedRowKeys ', () => {
const store = createStore({
selectedRowKeys: ['1'],
selectionDirty: false,
});
const instance = TestUtils.renderIntoDocument(
<SelectionBox
store={store}
rowIndex="1"
disabled={false}
onChange={() => {}}
defaultSelection={[]}
/>
);
expect(instance.state).toEqual({ checked: true });
});
it('checked by defaultSelection', () => {
const store = createStore({
selectedRowKeys: [],
selectionDirty: false,
});
const instance = TestUtils.renderIntoDocument(
<SelectionBox
store={store}
rowIndex="1"
disabled={false}
onChange={() => {}}
defaultSelection={['1']}
/>
);
expect(instance.state).toEqual({ checked: true });
});
it('checked when store change', () => {
const store = createStore({
selectedRowKeys: [],
selectionDirty: false,
});
const instance = TestUtils.renderIntoDocument(
<SelectionBox
store={store}
rowIndex="1"
disabled={false}
onChange={() => {}}
defaultSelection={[]}
/>
);
store.setState({
selectedRowKeys: ['1'],
selectionDirty: true,
});
expect(instance.state).toEqual({ checked: true });
});
})

91
tests/table/Table.test.js Normal file
View File

@ -0,0 +1,91 @@
import React from 'react';
import createStore from '../../components/table/createStore';
import Table from '../../components/table';
import TestUtils from 'react-addons-test-utils';
describe('Table', () => {
describe('row selection', () => {
it('allow select by checkbox', () => {
const columns = [{
title: 'Name',
dataIndex: 'name',
}];
const data = [{
name: 'Jack',
}, {
name: 'Lucy',
}];
const instance = TestUtils.renderIntoDocument(
<Table
columns={columns}
dataSource={data}
rowSelection={{}}
/>
);
const checkboxes = TestUtils.scryRenderedDOMComponentsWithTag(instance, 'input');
const checkboxAll = checkboxes[0];
checkboxAll.checked = true;
TestUtils.Simulate.change(checkboxAll);
expect(instance.store.getState()).toEqual({
selectedRowKeys: [0, 1],
selectionDirty: true,
});
checkboxes[1].checked = false;
TestUtils.Simulate.change(checkboxes[1]);
expect(instance.store.getState()).toEqual({
selectedRowKeys: [1],
selectionDirty: true,
});
checkboxes[1].checked = true;
TestUtils.Simulate.change(checkboxes[1]);
expect(instance.store.getState()).toEqual({
selectedRowKeys: [1, 0],
selectionDirty: true,
});
});
it('pass getCheckboxProps to checkbox', () => {
const columns = [{
title: 'Name',
dataIndex: 'name',
}];
const data = [{
id: 0,
name: 'Jack',
}, {
id: 1,
name: 'Lucy',
}];
const rowSelection = {
getCheckboxProps: record => ({
disabled: record.name === 'Lucy',
}),
};
const instance = TestUtils.renderIntoDocument(
<Table
columns={columns}
dataSource={data}
rowSelection={rowSelection}
rowKey="id"
/>
);
const checkboxes = TestUtils.scryRenderedDOMComponentsWithTag(instance, 'input');
expect(checkboxes[1].disabled).toBe(false);
expect(checkboxes[2].disabled).toBe(true);
});
});
})