Skip to content

Commit

Permalink
feat(react-grid): add the functionality to define a custom sorting al…
Browse files Browse the repository at this point in the history
…gorithm (#371)
  • Loading branch information
SergeyAlexeev authored Oct 2, 2017
1 parent 540fa95 commit 4ac8ab8
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 23 deletions.
38 changes: 24 additions & 14 deletions packages/dx-grid-core/src/plugins/sorting-state/computeds.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
import mergeSort from '../../utils/merge-sort';

const createSortingCompare = (sorting, compareEqual, getCellValue) => (a, b) => {
const inverse = sorting.direction === 'desc';
const { columnName } = sorting;
const aValue = getCellValue(a, columnName);
const bValue = getCellValue(b, columnName);

if (aValue === bValue) {
return (compareEqual && compareEqual(a, b)) || 0;
}

return (aValue < bValue) ^ inverse ? -1 : 1; // eslint-disable-line no-bitwise
const defaultCompare = (a, b) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
};

export const sortedRows = (rows, sorting, getCellValue) => {
export const sortedRows = (rows, sorting, getCellValue, getColumnCompare) => {
if (!sorting.length) return rows;

const compare = Array.from(sorting)
.reverse()
.reduce((prevCompare, columnSorting) =>
createSortingCompare(columnSorting, prevCompare, getCellValue), () => 0);
.reduce(
(prevCompare, columnSorting) => {
const { columnName } = columnSorting;
const inverse = columnSorting.direction === 'desc';
const columnCompare = (getColumnCompare && getColumnCompare(columnName)) || defaultCompare;

return (aRow, bRow) => {
const a = getCellValue(aRow, columnName);
const b = getCellValue(bRow, columnName);
const result = columnCompare(a, b);

if (result !== 0) {
return inverse ? -result : result;
}
return prevCompare(aRow, bRow);
};
},
() => 0,
);

return mergeSort(Array.from(rows), compare);
};
34 changes: 34 additions & 0 deletions packages/dx-grid-core/src/plugins/sorting-state/computeds.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,39 @@ describe('SortingState computeds', () => {
{ a: 1, b: 2 },
]);
});

it('can sort using custom compare', () => {
const getColumnCompare = jest.fn();

getColumnCompare.mockImplementation(() => (a, b) => {
if (a === b) {
return 0;
}
return a < b ? 1 : -1;
});
const sorting = [{ columnName: 'a', direction: 'desc' }];
const sorted = sortedRows(rows, sorting, getCellValue, getColumnCompare);

expect(getColumnCompare).toBeCalledWith(sorting[0].columnName);
expect(sorted).toEqual([
{ a: 1, b: 1 },
{ a: 1, b: 2 },
{ a: 2, b: 2 },
{ a: 2, b: 1 },
]);
});

it('should use default compare if custom compare returns nothing', () => {
const getColumnCompare = () => undefined;
const sorting = [{ columnName: 'a', direction: 'desc' }];
const sorted = sortedRows(rows, sorting, getCellValue, getColumnCompare);

expect(sorted).toEqual([
{ a: 2, b: 2 },
{ a: 2, b: 1 },
{ a: 1, b: 1 },
{ a: 1, b: 2 },
]);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import {
SortingState,
LocalSorting,
} from '@devexpress/dx-react-grid';
import {
Grid,
TableView,
TableHeaderRow,
} from '@devexpress/dx-react-grid-bootstrap3';

import {
generateRows,
employeeTaskValues,
} from '../../demo-data/generator';

const priorityWeights = {
Low: 0,
Normal: 1,
High: 2,
};

const comparePriority = (a, b) => {
const priorityA = priorityWeights[a];
const priorityB = priorityWeights[b];
if (priorityA === priorityB) {
return 0;
}
return (priorityA < priorityB) ? -1 : 1;
};

const getColumnCompare = columnName =>
(columnName === 'priority' ? comparePriority : undefined);

export default class Demo extends React.PureComponent {
constructor(props) {
super(props);

this.state = {
columns: [
{ name: 'subject', title: 'Subject', width: 300 },
{ name: 'startDate', title: 'Start Date' },
{ name: 'dueDate', title: 'Due Date' },
{ name: 'priority', title: 'Priority' },
],
rows: generateRows({
columnValues: employeeTaskValues,
length: 15,
}),
};
}
render() {
const { rows, columns } = this.state;

return (
<Grid
rows={rows}
columns={columns}
>
<SortingState />
<LocalSorting
getColumnCompare={getColumnCompare}
/>
<TableView />
<TableHeaderRow allowSorting />
</Grid>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { mount } from 'enzyme';
import Demo from './local-custom-sorting';

describe('BS3 sorting: custom local sorting demo', () => {
it('should work', () => {
mount(
<Demo />,
);
});
});
4 changes: 4 additions & 0 deletions packages/dx-react-demos/src/demo-registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ export const demos = {
bootstrap3: require('./bootstrap3/sorting/local-sorting-controlled').default,
'material-ui': require('./material-ui/sorting/local-sorting-controlled').default,
},
'local-custom-sorting': {
bootstrap3: require('./bootstrap3/sorting/local-custom-sorting').default,
'material-ui': require('./material-ui/sorting/local-custom-sorting').default,
},
'remote-sorting': {
bootstrap3: require('./bootstrap3/sorting/remote-sorting').default,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import {
SortingState,
LocalSorting,
} from '@devexpress/dx-react-grid';
import {
Grid,
TableView,
TableHeaderRow,
} from '@devexpress/dx-react-grid-material-ui';

import {
generateRows,
employeeTaskValues,
} from '../../demo-data/generator';

const priorityWeights = {
Low: 0,
Normal: 1,
High: 2,
};

const comparePriority = (a, b) => {
const priorityA = priorityWeights[a];
const priorityB = priorityWeights[b];
if (priorityA === priorityB) {
return 0;
}
return (priorityA < priorityB) ? -1 : 1;
};

const getColumnCompare = columnName =>
(columnName === 'priority' ? comparePriority : undefined);

export default class Demo extends React.PureComponent {
constructor(props) {
super(props);

this.state = {
columns: [
{ name: 'subject', title: 'Subject', width: 300 },
{ name: 'startDate', title: 'Start Date' },
{ name: 'dueDate', title: 'Due Date' },
{ name: 'priority', title: 'Priority' },
],
rows: generateRows({
columnValues: employeeTaskValues,
length: 15,
}),
};
}
render() {
const { rows, columns } = this.state;

return (
<Grid
rows={rows}
columns={columns}
>
<SortingState />
<LocalSorting
getColumnCompare={getColumnCompare}
/>
<TableView />
<TableHeaderRow allowSorting />
</Grid>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { mount } from 'enzyme';
import injectTapEventPlugin from 'react-tap-event-plugin';
import { MuiThemeProvider, createMuiTheme } from 'material-ui/styles';
import Demo from './local-custom-sorting';

injectTapEventPlugin();

describe('MUI sorting: custom local sorting demo', () => {
it('should work', () => {
mount(
<MuiThemeProvider theme={createMuiTheme()}>
<Demo />
</MuiThemeProvider>,
);
});
});
18 changes: 12 additions & 6 deletions packages/dx-react-grid/docs/guides/sorting.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

The Grid component supports sorting data by one or several column values. Use the corresponding plugins to manage the sorting state and sort data programmatically or via the UI (column headers and Group Panel).

Click several columns while holding `Shift` to sort data by these columns. Clicking a column while holding `Ctrl` (`Cmd` for MacOS) stops sorting by this column.
Click several columns while holding `Shift` to sort data by these columns. Clicking a column while holding `Ctrl` (`Cmd` for MacOS) cancels sorting by this column.

## Related Plugins

The following plugins implement sorting features:

- [SortingState](../reference/sorting-state.md) - controls the sorting state
- [LocalSorting](../reference/local-sorting.md) - performs local data sorting
- [TableHeaderRow](../reference/table-header-row.md) - renders the header row with sorting indicators
- [SortingState](../reference/sorting-state.md) - controls the sorting state
- [LocalSorting](../reference/local-sorting.md) - performs local data sorting
- [TableHeaderRow](../reference/table-header-row.md) - renders the header row with sorting indicators
- [GroupingPanel](../reference/grouping-panel.md) - renders the Group Panel with sorting indicators

Note that the [plugin order](./plugin-overview.md#plugin-order) is important.
Expand All @@ -23,7 +23,7 @@ Set the `TableHeaderRow` plugin's `allowSorting` property to true to enable chan

## Uncontrolled Mode

In the [uncontrolled mode](controlled-and-uncontrolled-modes.md), specify the initial sorting conditions in the `SortingState` plugin's `defaultSorting` property.
In the [uncontrolled mode](controlled-and-uncontrolled-modes.md), specify the initial sorting conditions in the `SortingState` plugin's `defaultSorting` property.

.embedded-demo(sorting/local-header-sorting)

Expand All @@ -41,9 +41,15 @@ Note that the `LocalGrouping` plugin should follow the `LocalSorting` to provide

.embedded-demo(sorting/local-group-sorting)

## Custom Sorting Algorithm

The [LocalSorting](../reference/local-sorting.md) plugin's `getColumnCompare` property allows you to implement a custom sorting algorithm. If the `getColumnCompare` function returns undefined, it applies the default sorting algorithm.

.embedded-demo(sorting/local-custom-sorting)

## Remote Sorting

You can handle the Grid sorting state changes to request data from the server with the corresponding sorting applied if your data service supports sorting operations.
It is possible to perform sorting remotely by handling sorting state changes, generating a request, and sending it to the server.

Sorting options are updated once an end-user interacts with a column header in the header row or Group Panel. Handle sorting option changes using the `SortingState` plugin's `onSortingChange` event and request data from the server using the applied sorting options. Once the sorted data is received from the server, pass it to the `Grid` component's `rows` property.

Expand Down
9 changes: 8 additions & 1 deletion packages/dx-react-grid/docs/reference/local-sorting.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ A plugin that performs local data sorting.

### Properties

none
Name | Type | Default | Description
-----|------|---------|------------
getColumnCompare | (columnName: string) => [Compare](#compare) &#124; undefined | | A function implementing custom sorting. See the [Sorting guide](../guides/sorting.md#custom-sorting-algorithm) for more information.

## Interfaces
### <a name="compare"></a>Compare
A function with the following signature `(a: any, b: any) => number`


## Plugin Developer Reference

Expand Down
15 changes: 13 additions & 2 deletions packages/dx-react-grid/src/plugins/local-sorting.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Getter, PluginContainer } from '@devexpress/dx-react-core';
import { sortedRows } from '@devexpress/dx-grid-core';

const pluginDependencies = [
{ pluginName: 'SortingState' },
];

const rowsComputed = ({ rows, sorting, getCellValue }) => sortedRows(rows, sorting, getCellValue);

// eslint-disable-next-line react/prefer-stateless-function
export class LocalSorting extends React.PureComponent {
render() {
const { getColumnCompare } = this.props;
const rowsComputed = ({ rows, sorting, getCellValue }) =>
sortedRows(rows, sorting, getCellValue, getColumnCompare);

return (
<PluginContainer
pluginName="LocalSorting"
Expand All @@ -21,3 +24,11 @@ export class LocalSorting extends React.PureComponent {
);
}
}

LocalSorting.propTypes = {
getColumnCompare: PropTypes.func,
};

LocalSorting.defaultProps = {
getColumnCompare: undefined,
};

0 comments on commit 4ac8ab8

Please sign in to comment.