Sortabular's helpers make it possible to manage sorting related classes and to sort rows based on them.
The API consists of the sort helpers, transforms, formatters, and strategies. These can be combined together to set up sort in various ways. There's control over the algorithm used as well as how it's bound to the user interface. You can also control how sorting is tracked (per column index or per property).
import * as sort from 'sortabular';
// Or you can cherry-pick
import { byColumn } from 'sortabular';
import { byColumn as sortByColumn } from 'sortabular';
sort.byColumn({ sortingColumns: <sorting columns>, sortingOrder: {FIRST: <string>, <string>: <string>}, selectedColumn: <string> }) => <sorting colums> || {}
sort.byColumn
allows you to sort per one column. It discards possible existing sorting state. If you are trying to sort the same column, it will cycle between ascending, descending, and no sorting. In case you are trying to sort some other column, it will start from the ascending state while discarding the existing sorting state.
sort.byColumns({ sortingColumns: <sorting columns>, sortingOrder: {FIRST: <string>, <string>: <string>}, selectedColumn: <string> }) => <sorting columns> || {}
sort.byColumns
is like sort.byColumn
except it doesn't discard possible existing sort state and instead accumulates it. This allows you to perform sorting over multiple columns while refining the results. The last-sorted column always has the highest position
value, i.e. the lowest sorting priority.
sort.byColumnsPrioritizeLastSorted({ sortingColumns: <sorting columns>, sortingOrder: {FIRST: <string>, <string>: <string>}, selectedColumn: <string> }) => <sorting columns> || {}
sort.byColumnsPrioritizeLastSorted
is like sort.byColumns
except it
always gives the last-sorted column the highest sorting priority.
sort.sorter({ columns: [<object>], sortingColumns: <sorting columns>, sort: <function>, strategy = strategies.byIndex })([<rows to sort>]) => [<sorted rows>]
sort.sorter
sorts the passed rows
using a sortingColumns
definitions and a sort
function. It has been designed to work based on lodash.orderBy signature.
If you want to evaluate columns in a reverse order instead of the default, you can reverse sort
function like this:
const reverseSort = (data, columnIndexList, orderList) => (
orderBy(data, columnIndexList.slice().reverse(), orderList.slice().reverse())
);
sort.sort = ({ event = 'onClick', getSortingColumns = () => [], strategy = strategies.byIndex, onSort = (columnIndex) => {} } = {}) => (value, { columnIndex }, props)
sort.sort
can be applied as a transform. It expects getSortingColumns
and onSort
callbacks. The former should return the sorting column data, the latter is called when the user sorts based on event
.
sort.reset = ({ event = 'onDoubleClick', getSortingColumns = () => [], strategy = strategies.byIndex, onReset = (columnIndex) => {} } = {}) => (value, { columnIndex }, props)
sort.reset
can be applied as a transform. It expects getSortingColumns
and onReset
callbacks. The former should return the sorting column data, the latter is called when the user sorts based on event
.
sort.header = ({ sortable, strategy = strategies.byIndex, getSortingColumns = () => [] }) => (value, { columnIndex })
sort.header
can be used to sort within a header cell. This works well with sort.reset
since then you can apply both reseting and sorting to the same cell without conflicts. It expects an initialized sortable (i.e., sort.sort
) and getSortingColumns
. If sorting is active at a column, it displays the current order number.
sortable
and strategy
parameters are optional. getSortingColumns
is required.
You can customize props
of sort.header
specific portions through the following protocol:
props = {
container: {},
value: {},
order: {}
}
sort.order = ({ strategy = strategies.byIndex, getSortingColumns = () => [] }) => (value, { columnIndex })
sort.order
can be used to display a sorting arrow widget. It figures out the class name based on the current sorting status. sort.header
uses this internally.
Most of the functions accept a strategy. This allows you to modify their sorting behavior. By default they'll track sorting by column index. It's possible to change it to sort by property.
sort.strategies.byIndex
byIndex
is the default strategy used by other functions. It literally means the system will track sorting per index.
sort.strategies.byProperty
byProperty
ties sorting state to column property. This can be useful if you want to retain the sorting state within a column while moving it around.
Sorting relies on a structure like this to describe what is being sorted and in what order:
const sortingColumns = {
0: {
direction: 'asc',
position: 1
},
1: {
direction: 'desc',
position: 0
}
};
It maps column index to sorting state and can contain multiple sorters.
It is possible to customize the sorting order of sort.byColumn
and sort.byColumns
by passing an object describing the sorting. It should contain FIRST
key to describe the starting point. The remaining key-value pairs should form a cycle.
Assuming you are using the sort
transform, the order values are used for generating the classes you see at the user interface.
The default order cycles between asc
, desc
, and ''
(no sort).
You could implement a custom order cycling between asc
and desc
like this:
const sortingOrder = {
FIRST: 'asc',
asc: 'desc',
desc: 'asc'
};
The sort
transform has been designed to track when the user requests sorting and render possibly matching sorting condition as a class for styling. In addition you will need to use specific sort helpers to handle the sorting logic. The helpers have been encapsulated within the sort
module.
Example:
...
import * as sort from 'sortabular';
...
const sortable = sort.sort({
// Point the transform to your rows. React state can work for this purpose
// but you can use a state manager as well.
getSortingColumns: () => this.state.sortingColumns || [],
// The user requested sorting, adjust the sorting state accordingly.
// This is a good chance to pass the request through a sorter.
onSort: selectedColumn => {
this.setState({
sortingColumns: sort.byColumns({ // sort.byColumn would work too
sortingColumns: this.state.sortingColumns,
selectedColumn
})
});
}
});
...
// Mark a header as sortable
columns: [
{
property: 'name',
header: {
label: 'name',
transforms: [sortable]
}
}
]
The general workflow goes as follows:
- Set up the
sort
transform. Its purpose is to track when the user requests sorting and render possibly matching sorting condition as a class for styling. - Set up a sort helper. There are helpers for sorting per one column (
sort.byColumn
) and one for sorting per multiple columns (sort.byColumns
). The helpers handle managing sorting conditions and actual sorting. If you have a back-end, you can skip the latter. - Sort the rows before rendering.
- Feed the sorted rows to a
Table
.
You can find suggested default styling for the package at
style.css
in the package root.
Example:
/*
import React from 'react';
import orderBy from 'lodash/orderBy';
import * as resolve from 'table-resolver';
import * as Table from 'reactabular-table';
import * as sort from 'sortabular';
import { compose } from 'redux';
*/
const initialRows = [
{
id: 100,
name: {
first: 'Adam',
last: 'West'
},
age: 10
},
{
id: 101,
name: {
first: 'Brian',
last: 'Eno'
},
age: 43
},
{
id: 102,
name: {
first: 'Brian',
last: 'Wilson'
},
age: 23
},
{
id: 103,
name: {
first: 'Jake',
last: 'Dalton'
},
age: 33
},
{
id: 104,
name: {
first: 'Jill',
last: 'Jackson'
},
age: 63
}
];
class SortTable extends React.Component {
constructor(props) {
super(props);
const getSortingColumns = () => this.state.sortingColumns || {};
const sortable = sort.sort({
// Point the transform to your rows. React state can work for this purpose
// but you can use a state manager as well.
getSortingColumns,
// The user requested sorting, adjust the sorting state accordingly.
// This is a good chance to pass the request through a sorter.
onSort: selectedColumn => {
this.setState({
sortingColumns: sort.byColumns({ // sort.byColumn would work too
sortingColumns: this.state.sortingColumns,
selectedColumn
})
});
},
// Use property strategy over index one given we have nested data
strategy: sort.strategies.byProperty
});
const resetable = sort.reset({
event: 'onDoubleClick',
getSortingColumns,
onReset: ({ sortingColumns }) => this.setState({ sortingColumns }),
strategy: sort.strategies.byProperty
});
this.state = {
// Sort the first column in a descending way by default.
// "asc" would work too and you can set multiple if you want.
sortingColumns: {
'name.first': {
direction: 'desc',
position: 0
}
},
columns: [
{
header: {
label: 'Name'
},
children: [
{
property: 'name.first',
header: {
label: 'First Name',
transforms: [resetable],
formatters: [
sort.header({
sortable,
getSortingColumns,
strategy: sort.strategies.byProperty
})
]
}
},
{
property: 'name.last',
header: {
label: 'Last Name',
transforms: [resetable],
formatters: [
sort.header({
sortable,
getSortingColumns,
strategy: sort.strategies.byProperty
})
]
}
}
]
},
{
property: 'age',
header: {
label: 'Age',
transforms: [resetable],
formatters: [
sort.header({
sortable,
getSortingColumns,
strategy: sort.strategies.byProperty
})
]
// Alternative if you don't need reset.
// transforms: [sortable]
}
}
],
rows: initialRows
};
}
render() {
const { rows, sortingColumns, columns } = this.state;
const resolvedColumns = resolve.columnChildren({ columns });
const sortedRows = compose(
sort.sorter({
columns: resolvedColumns,
sortingColumns,
sort: orderBy,
strategy: sort.strategies.byProperty
}),
resolve.resolve({
columns: resolvedColumns,
method: resolve.nested
})
)(rows);
return (
<div>
<Table.Provider columns={resolvedColumns}>
<Table.Header
headerRows={resolve.headerRows({ columns })}
/>
<Table.Body rows={sortedRows} rowKey="id" />
</Table.Provider>
</div>
);
}
}
<SortTable />
MIT. See LICENSE for details.