Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save codekiln/c33305b4824608d643ef1d1bd5bcd436 to your computer and use it in GitHub Desktop.
Save codekiln/c33305b4824608d643ef1d1bd5bcd436 to your computer and use it in GitHub Desktop.
ImmutableJS React Table With Multiple Sort And Styled Components

ImmutableJS React Table With Multiple Sort And Styled Components

An example of how to make a multi-sort React table with ImmutableJS and Styled Components, using ImmutableJS Records.

A Pen by Myer Nore on CodePen.

License.

<div id="app"></div>
/* ImmutableJS React Table With Multiple Sort And Styled Components
An example of how to make a multi-sort React table
with ImmutableJS and Styled Components, using
custom, extended ImmutableJS Records.
Click on a column in the header row to sort that row.
You can sort more than one row, though at this time
the sort is left to right by nature.
Consider changing the columns passed to the table
using Faker below.
Uses:
* React - https://reactjs.org/
* ImmutableJS - https://facebook.github.io/immutable-js/
* Styled Components - https://www.styled-components.com/
* Faker.js - https://github.com/marak/Faker.js/
Table of Contents
* Part 1: Data For Table
* Part 2: ImmutableJS Records
* Part 3: Table Stateful Container
* Part 4: Pure Table Components
* Part 5: Table Presentational Components
* Part 6: React Root Render
***************************************/
//////////// Part 1: Data For Table ///////////////
/**
* The data for the table is generated using faker.js
* See API Docs here:
* https://rawgit.com/Marak/faker.js/master/examples/browser/index.html
* Feel free to adjust the columns to your liking
*/
const peopleData = new Array(50).fill(0).map(generateFakePerson)
function generateFakePerson() {
return {
"region": getRegion(),
"prefix": faker.name.prefix(),
"first_name": faker.name.firstName(),
"last_name": faker.name.lastName(),
"suffix": faker.name.suffix(),
"avatar": faker.image.avatar()
}
}
function getRegion() {
return faker.random.arrayElement([
"New England",
"Mideast",
"Great Lakes",
"Plains",
"Southeast",
"Southwest",
"Rocky Mountain",
"Far West"
])
}
//////////// Part 2: ImmutableJS Records ///////////////
/**
* ImmutableJS Records let you use typical JS
* dot notation to access properties.
* This declares a new Person to have the defaults
* of the first person in the list.
*
* We'll use the fact that it's a Record to get
* access to its named attributes to store
* metadata for the Columns (see below).
**/
const Person = Immutable.Record(peopleData[0]);
/**
* This list of records is what gets passed to
* the table for display. The contents of the
* people variable is an ImmutableJS List of
* Person records.
**/
const people = Immutable.fromJS(peopleData).map(Person);
/**
* ImmutableJS Model representing a column in the table.
* This stores column metadata for the table,
* including the sort direction of each column.
**/
class Column extends Immutable.Record({
id: null,
name: null,
direction: null,
isImage: false,
}) {
/**
* Return -1 if this column has a direction of
* descending. Return 1 if this column has a
* direction of ascending.
* Return 0 otherwise.
* Used by column.compare() for sorting a column.
**/
get dirNum() {
return this.direction
? this.direction === 'descending'
? -1
: this.direction === 'ascending'
? 1
: 0
: 0;
}
/**
* Find the ordering of two items in this column.
* Used to sort collection of columns.
* Called by Column.getComparatorFor(columns).
* See https://facebook.github.io/immutable-js/docs/#/List/sort
*/
compare(rowA_object, rowB_object) {
if (this.direction) {
const dirNum = this.dirNum;
// every column stores the name of the attribute
// that it accesses on the row object.
// this compares the value of that attribute in
const rowA_value = rowA_object[this.name];
const rowB_value = rowB_object[this.name]
if(rowA_value < rowB_value) {
return dirNum * 1;
}
if(rowA_value > rowB_value) {
return dirNum * -1;
}
}
return 0
}
/**
* Return a React component.setState(action)
* action function which toggles the sort
* setting on this specific column.
*/
getCycleColumnSortAction() {
return (state, props) => {
const columns = state.columns.updateIn(
[this.id, 'direction'], dir =>
dir === ''
? 'descending'
: dir === 'descending'
? 'ascending'
: ''
)
const records = state.records.sort(
Column.getComparatorFor(columns)
)
return {columns, records}
};
}
/*
* Get a comparator function that can
* be used as an input to
* ImmutableJS.List.sort(comparator).
* https://facebook.github.io/immutable-js/docs/#/List/sort
*/
static getComparatorFor(columns) {
return (a, b) => {
for (const col of columns.toSeq()) {
const comparison = col.compare(a, b)
if (comparison) return comparison
}
return 0;
}
}
}
//////////// Part 3: Table Stateful Container
class Table extends React.Component {
constructor(props) {
super(props);
// find and store a stateful representation
// of the attrs from the first record
// in an ImmutableJS List of Column records
const firstRow = props.records.get(0);
const firstRowAttrs = firstRow.keySeq().toArray();
const columnData = firstRowAttrs.map((name, id) => ({
id, name, direction: '',
isImage: firstRow.get(name).endsWith('.jpg')
}));
const columns = Immutable.fromJS(columnData)
.map(c => new Column(c));
// the state for this container component
// consists of the records that will form the rows
// as well as the column metadata about the dataset
// passed to the table.
this.state = {
records: props.records,
columns: columns
}
this.onHeaderClick = this.onHeaderClick.bind(this)
}
/**
* When a header cell is clicked, the column for
* that cell is passed as an argument.
* The Column ImmutableJS record defines the
* getCycleColumnSortAction method that returns
* a react setState action function that mutates
* the table state in a functionally pure manner.
**/
onHeaderClick(column) {
this.setState(column.getCycleColumnSortAction());
}
render() {
const {records, columns} = this.state;
const rows = records.map((record, i) => {
return (
<Row
record={record}
columns={columns}
/>
);
});
return (
<AppContainer>
<table>
<thead>
<Row columns={columns}
onHeaderClick={this.onHeaderClick}/>
</thead>
<tbody>
{rows}
</tbody>
</table>
</AppContainer>
);
}
}
//////////// Part 4: Pure Table Components //////
/**
* Render a row.
* If a row is a header row, pass down the function
* used to handle clicks to a header cell.
**/
class Row extends React.PureComponent {
render() {
const {record, columns, onHeaderClick} = this.props;
const cells = columns.map( col =>
record
? (<Cell column={col}>{record[col.name]}</Cell>)
: (<Cell column={col} onHeaderClick={onHeaderClick}
isHeaderRow={true}/>)
);
return (<HoverableRow>{cells}</HoverableRow>);
}
}
/**
* Render a cell, which knows is passed the Column
* record that helpes it know whether to render an
* image, a header cell or a plain Td.
**/
class Cell extends React.PureComponent {
render() {
const {column, onHeaderClick, isHeaderRow} = this.props;
if(isHeaderRow) {
const sortArrow = column.direction
? (<SortArrow sorted={column.direction}/>)
: '';
return (
<ColumnHeader onClick={onHeaderClick.bind(null, column)}>
{ column.name.replace('_', ' ') } { sortArrow }
</ColumnHeader>
);
}
if (column.isImage) {
return (
<Td><Avatar src={ this.props.children }/></Td>
);
}
// just a plain cell
return (
<Td>{ this.props.children }</Td>
);
}
}
/**
* Microcomponent that renders an arrow
* pointing up, pointing down, or hidden
* depending on whether it is passed
* `sorted=['ascending', 'descending', '']`
**/
class SortArrow extends React.PureComponent {
render() {
const { sorted } = this.props;
const sortArrow = sorted == 'ascending'
? ' ⇧'
: sorted == 'descending' ? ' ⇩' : '';
return (<span> { sortArrow } </span>);
}
}
/////// Part 5: Table Presentational Components ///
// Styles are defined using Styled-Components JS
// See https://www.styled-components.com/
// normally, one would import styled components
// from an npm module, but here, we'll select
// the default export manually since we're on codepen.
const styled = styled.default;
/**
* Define the default styles for the whole application
**/
const AppContainer = styled.div`
div, table {
font-family: 'Roboto', sans-serif;
border-collapse: collapse;
}
`
/**
* Define the styles for a row. Called
* a HoverableRow because we already have a Row
* pure component defined above.
**/
const HoverableRow = styled.tr`
tbody &:hover {
background: palevioletred;
}
`
/**
* Define the styles for a table data
* cell.
**/
const Td = styled.td`
color: #666;
text-align: left;
padding: 5px;
tbody & {
border-top: solid #E67E22 1px;
}
`;
/**
* Define the styles for an image used
* in the table.
**/
const Avatar = styled.img`
width: 50px;
height: 50px;
`
/**
* Define the styles for a column header.
**/
const ColumnHeader = Td.extend`
font-weight: bold;
font-family: 'Andika', sans-serif;
text-decoration: underline #717D7E;
text-transform: capitalize;
cursor: pointer;
`
////////////// Part 6: React Render /////////////////
ReactDOM.render(
(<Table records={people}/>),
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/styled-components/2.1.2/styled-components.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.5.10/prop-types.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.2/immutable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/faker.min.js"></script>
/* https://fonts.google.com/specimen/Andika?selection.family=Andika */
@import url('https://fonts.googleapis.com/css?family=Andika|Roboto');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment