Material-ui: Sortable Columns in Table

Created on 6 Aug 2015  路  71Comments  路  Source: mui-org/material-ui

I would like there to be sortable columns for the data table similar to those shown in google material design: http://www.google.com/design/spec/components/data-tables.html#data-tables-interaction

enhancement

Most helpful comment

Something Like That

import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn, TableFooter } from 'material-ui/Table';
import { SmartTableRow } from 'components';
import React, { PropTypes, Component } from 'react';
import styles from './SmartTable.scss';
import SortIcon from 'material-ui/svg-icons/action/swap-vert';
import IconButton from 'material-ui/IconButton';
import ChevronLeft from 'material-ui/svg-icons/navigation/chevron-left';
import ChevronRight from 'material-ui/svg-icons/navigation/chevron-right';
import getMuiTheme from 'material-ui/styles/getMuiTheme';

function sortFunc(a, b, key) {
  if (typeof(a[key]) === 'number') {
    return a[key] - b[key];
  }

  const ax = [];
  const bx = [];

  a[key].replace(/(\d+)|(\D+)/g, (_, $1, $2) => { ax.push([$1 || Infinity, $2 || '']); });
  b[key].replace(/(\d+)|(\D+)/g, (_, $1, $2) => { bx.push([$1 || Infinity, $2 || '']); });

  while (ax.length && bx.length) {
    const an = ax.shift();
    const bn = bx.shift();
    const nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]);
    if (nn) return nn;
  }

  return ax.length - bx.length;
}

class SmartTable extends Component {

  static childContextTypes = {
    muiTheme: React.PropTypes.object.isRequired
  }

  constructor(props, context) {
    super(props, context);
    this.state = { isAsc: false, sortHeader: null };
  }

  getChildContext() {
    return { muiTheme: getMuiTheme() };
  }

  sortByColumn(column, data) {
    const isAsc = this.state.sortHeader === column ? !this.state.isAsc : true;
    const sortedData = data.sort((a, b) => sortFunc(a, b, column));

    if (!isAsc) {
      sortedData.reverse();
    }

    this.setState({
      data: sortedData,
      sortHeader: column,
      isAsc
    });
  }

  render() {

    const { offset, limit, total, tableHeaders, data, onPageClick } = this.props;

    return (
      <Table className={ styles.table } selectable={false}>
        <TableHeader displaySelectAll ={false} adjustForCheckbox={false}>
          <TableRow>
            {!!tableHeaders && tableHeaders.map((header, index) => (
              <TableHeaderColumn key={index}>
                <div className={styles.rowAlign}>
                  {header.alias}
                  <SortIcon
                    id={header.dataAlias}
                    className={styles.sortIcon}
                    onMouseUp={(e) => this.sortByColumn(e.target.id, data) }
                  />
                </div>
              </TableHeaderColumn>
            ))}
          </TableRow>
        </TableHeader>
        <TableBody showRowHover stripedRows displayRowCheckbox={false}>
          {!!data && data.map((row, index) => (
            <SmartTableRow key={index} {...{ row, index, tableHeaders }} />
          ))}
        </TableBody>
        <TableFooter>
          <TableRow>
            <TableRowColumn>
                <div className={styles.footerControls}>
                  { `${Math.min((offset + 1), total)} - ${Math.min((offset + limit), total)} of ${total}` }
                  <IconButton disabled={offset === 0} onClick={onPageClick.bind(null, offset - limit)}>
                    <ChevronLeft/>
                  </IconButton>
                  <IconButton disabled={offset + limit >= total} onClick={onPageClick.bind(null, offset + limit)}>
                    <ChevronRight/>
                  </IconButton>
                </div>
              </TableRowColumn>
          </TableRow>
        </TableFooter>
      </Table>
    );
  }
}

SmartTable.propTypes = {
  tableHeaders: PropTypes.array,
  data: PropTypes.array,
  offset: PropTypes.number, // current offset
  total: PropTypes.number, // total number of rows
  limit: PropTypes.number, // num of rows in each page
  onPageClick: PropTypes.func // what to do after clicking page number
};

export default SmartTable;

.table {
  width: auto;
  padding-top: 30px;
}

.rowAlign {
  display: flex;
  align-items: center;
}

.footerControls {
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.sortIcon {
  cursor: pointer;
  path {
    fill: rgb(158, 158, 158) !important;
    pointer-events: none;
  }
}

All 71 comments

If you need this immediately, you can try working with the table from branch v0.11 and create your own column header and use click/tap events to determine how to sort the table rows (ascending/descending). I'm on a bit of flux/reflux/redux kick and would like to see the sorting logic outside of MUI. It should not be difficult to provide the column header sort indicators and expose callbacks for those events. We'd be happy to accept a PR (against branch v0.11) if you have some time.

Anybody working on this? Our team(@VladimirPal) would love to give it a shot, using @jkruder 's approach.

I think we just need sortIndicator and onClick for TableHeaderColumn, sortIndicator can be whatever component(usually a FontIcon) user passes in. The sorting logic would be handled outside of Table.

Sorting logic can be complicated, e.g. single column sorting, multi-column sorting, and the order of sorting cycle(Asc->Desc->None or Desc->Asc), and column priority in multi-column sorting. Maybe it's better to keep MUI's Table lean and move the logic out. Your thought? @jkruder @sjstebbins

@zachguo Definitely agree with keeping the sorting logic outside of the table. I would consider a default sort array of ['asc', 'desc', 'none'] and every click on the column header would progress the index which will wrap back to 0 and make a call to a CB with the sort value and column name/number/identifier. This array could be supplied as a prop for custom values.

Multi column sorting can be handled by the consumer of the table. I've seen priority given to the order the columns are clicked. Could be maintained by the consumer as an array of objects: [{columnId: 'asc'}, {otherColumnId: 'desc'}]. I would add a multiColumnSortable (feel free to change the name) field to control multiple column sorting.

@jkruder TBH I'm not sure whether it's a good idea to save a sort array into Table. I'm thinking of a lower-level approach, by making both indicator and onClick decoupled from sorting logic, so that

  • Table component would be used purely for rendering (hence easier to reason about),
  • both props can serve other needs like giving a badge to column headers, or highlighting a column by clicking its header.

But cons are that APIs would not be very handy for common users.

@zachguo Good point about the indicator; all about decoupling the UI. We could do as you suggest and create an unopinionated version of the table with an indicator and onClick and then create a sortable table in the docs to demonstrate the API. Worst case, we can provide a SortableTable component if we find that the users are not finding the API intuitive.

Yup, SortableTable is a good idea.

We'll use icon instead of indicator as shown in In MD's DataTable specs:
screen shot 2015-08-25 at 6 37 45 pm
screen shot 2015-08-25 at 6 38 48 pm

@zachguo whats the status of this?

@VladimirPal has developed one which supports both sorting and pagination, without changing single line of MUI codes. We'll test it out and port it here when we think it's ready.

I would love to see this, just upgraded to 0.11 to play with the tables

Nice work

@zachguo was going to start building a sortable, pageable table myself but see you've got something working. When do you think it'll be good to go? Happy to use something in the meantime outside of the material-ui trunk.

@zachguo @VladimirPal any updates on the status of this?

@shaurya947 @daniel-sim @sjstebbins

We did sorting(both single-column and multi-column sorting) and pagination on both server-side and client-side, but found it hard to refactor these new functionalities into MUI as easy-to-use APIs without losing composibility that MUI currently has.

One can actually implement sorting & pagination by composing MUI's table components without writing too many codes. The general idea is to keep track of current data/sort/page by yourself, and let MUI's table components purely render them.

IMHO, instead of providing high-level APIs such as sorting and pagination, keeping current low-level APIs is the way to go. Less overhead, easier to compose. However, to make rendering sorting and pagination easier, we may add an icon/indicator prop and an onClick event to TableHeaderColumn, and even a new pre-styled TableFooter component.

Your thoughts?

cc: @oliviertassinari

@zachguo:

One can actually implement sorting & pagination by composing MUI's table components without writing too many codes. The general idea is to keep track of current data/sort/page by yourself, and let MUI's table components purely render them.

Yes, that is quite doable.

The props that you mentioned are rather general purpose and could also come in handy in other scenarios besides sorting. So feel free to write up a PR for that..

This might not be ideal, but I was able to implement sorting by just including a div with an onClick inside the TableHeaderColumn. Fixing the onClick behavior for TableHeaderColumn would be awesome, and IMO totally enough for 99% of the cases.

@roieki this is what we did too

@shaurya947 @jkruder There's actually an onClick prop for TableHeaderColumn but it's not working. related #2011

Does anybody want to take that up?

Something Like That

import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn, TableFooter } from 'material-ui/Table';
import { SmartTableRow } from 'components';
import React, { PropTypes, Component } from 'react';
import styles from './SmartTable.scss';
import SortIcon from 'material-ui/svg-icons/action/swap-vert';
import IconButton from 'material-ui/IconButton';
import ChevronLeft from 'material-ui/svg-icons/navigation/chevron-left';
import ChevronRight from 'material-ui/svg-icons/navigation/chevron-right';
import getMuiTheme from 'material-ui/styles/getMuiTheme';

function sortFunc(a, b, key) {
  if (typeof(a[key]) === 'number') {
    return a[key] - b[key];
  }

  const ax = [];
  const bx = [];

  a[key].replace(/(\d+)|(\D+)/g, (_, $1, $2) => { ax.push([$1 || Infinity, $2 || '']); });
  b[key].replace(/(\d+)|(\D+)/g, (_, $1, $2) => { bx.push([$1 || Infinity, $2 || '']); });

  while (ax.length && bx.length) {
    const an = ax.shift();
    const bn = bx.shift();
    const nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]);
    if (nn) return nn;
  }

  return ax.length - bx.length;
}

class SmartTable extends Component {

  static childContextTypes = {
    muiTheme: React.PropTypes.object.isRequired
  }

  constructor(props, context) {
    super(props, context);
    this.state = { isAsc: false, sortHeader: null };
  }

  getChildContext() {
    return { muiTheme: getMuiTheme() };
  }

  sortByColumn(column, data) {
    const isAsc = this.state.sortHeader === column ? !this.state.isAsc : true;
    const sortedData = data.sort((a, b) => sortFunc(a, b, column));

    if (!isAsc) {
      sortedData.reverse();
    }

    this.setState({
      data: sortedData,
      sortHeader: column,
      isAsc
    });
  }

  render() {

    const { offset, limit, total, tableHeaders, data, onPageClick } = this.props;

    return (
      <Table className={ styles.table } selectable={false}>
        <TableHeader displaySelectAll ={false} adjustForCheckbox={false}>
          <TableRow>
            {!!tableHeaders && tableHeaders.map((header, index) => (
              <TableHeaderColumn key={index}>
                <div className={styles.rowAlign}>
                  {header.alias}
                  <SortIcon
                    id={header.dataAlias}
                    className={styles.sortIcon}
                    onMouseUp={(e) => this.sortByColumn(e.target.id, data) }
                  />
                </div>
              </TableHeaderColumn>
            ))}
          </TableRow>
        </TableHeader>
        <TableBody showRowHover stripedRows displayRowCheckbox={false}>
          {!!data && data.map((row, index) => (
            <SmartTableRow key={index} {...{ row, index, tableHeaders }} />
          ))}
        </TableBody>
        <TableFooter>
          <TableRow>
            <TableRowColumn>
                <div className={styles.footerControls}>
                  { `${Math.min((offset + 1), total)} - ${Math.min((offset + limit), total)} of ${total}` }
                  <IconButton disabled={offset === 0} onClick={onPageClick.bind(null, offset - limit)}>
                    <ChevronLeft/>
                  </IconButton>
                  <IconButton disabled={offset + limit >= total} onClick={onPageClick.bind(null, offset + limit)}>
                    <ChevronRight/>
                  </IconButton>
                </div>
              </TableRowColumn>
          </TableRow>
        </TableFooter>
      </Table>
    );
  }
}

SmartTable.propTypes = {
  tableHeaders: PropTypes.array,
  data: PropTypes.array,
  offset: PropTypes.number, // current offset
  total: PropTypes.number, // total number of rows
  limit: PropTypes.number, // num of rows in each page
  onPageClick: PropTypes.func // what to do after clicking page number
};

export default SmartTable;

.table {
  width: auto;
  padding-top: 30px;
}

.rowAlign {
  display: flex;
  align-items: center;
}

.footerControls {
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.sortIcon {
  cursor: pointer;
  path {
    fill: rgb(158, 158, 158) !important;
    pointer-events: none;
  }
}

@vorlov nice example, thanks. Could you post your SmartTableRow component as well?

@chrisrittelmeyer sure, also updated the code for table above

import { TableRow, TableRowColumn } from 'material-ui/Table';
import React, { PropTypes } from 'react';
import formatTableCell from './formatTableCell';

const SmartTableRow = ({ index, row, tableHeaders }) => (
  <TableRow key={index}>
    {tableHeaders.map((header, propIndex) => (
      <TableRowColumn key={propIndex}>{formatTableCell(row[header.dataAlias], header.format)}</TableRowColumn>
    ))}
  </TableRow>
);

SmartTableRow.propTypes = {
  index: PropTypes.number,
  row: PropTypes.object
};

export default SmartTableRow;

@vorlov do you have this code up and working anywhere, perhaps?

@chrisrittelmeyer stange question) sure I have)

Oh! I should have been more specific - do you have it in an environment that you can link us to? The above code is still missing some dependencies, so rather than pasting all the parts here, it might be easier to just point me to a repo.

@vorlov This is a really nice implementation of the Smart Table, I don't quite understand the pagination however. I don't see any logic to handle the offset, limit, and total before render?

@JK82 It's up to you, I am not using pagination on my project, added it for future implementation.
It could be done in componentWillReceiveProps or componentWillMount

@vorlov Sweet, again this is really nice work

@vorlov hmm this approach directly affects props (redux store) by reference :\

@NeXTs what do you mean?

@vorlov
const sortedData = data.sort((a, b) => sortFunc(a, b, column));
data.sort - sort is mutator so it modifies data field which is direct reference to this.props.data, isn't it?

What was the reason to do?

this.setState({
   data: sortedData
})

if render doesn't use data from state, it uses const { data } = this.props; only
maybe idea was to get data from props, clone it, store it at state and then sort/revert data in state?

TableSortLabel has been added in next to help with this. There is also a demo on next branch that has sorted columns.

@NeXTs Actually I don't know where did you see redux store there. I pass data in new object so it doesn't affect redux store.

Props usually comes from redux store, that was my point
Okay nevermind, it's not so important now, thank you anyway! :+1:

@NeXTs I pass props to table using object spread, so state is not mutated.
<SmartTable { ...{ tableHeaders, data, limit: 20, total: !!data && data.length, offset: 0, onPageClick: this.handleLoad } } />

@vorlov Got it
@nathanmarks oh cool! When it will be available in master branch?

one dependency is missing in the above
import formatTableCell from './formatTableCell';

@jimgong92 @chrisrittelmeyer formatTableCell file

import numeral from 'numeral';
import React from 'react';
import { Link } from 'react-router';
import FlatButton from 'material-ui/FlatButton';

export default (cell, format, row) => {
  switch (format && format.type) {
    case 'link':
      return <Link to={ `${format.url}${row.id}` }>{ cell }</Link>;    
    case 'percentage':
      return `${cell}%`;    
    case 'money':
      return numeral(cell).format('0,0');
    default:
      return cell;
  }
};

Thanks for the update.

@vorlov ur component is great, u should put it inside a gist or a repo example

@nathanmarks where is the link for TableSortLabel and the demo of it on next branch?

Yup there are no traces of TableSortLabel or any sortability in the repo. Wondering why this issue was closed. Thanks @vorlov for your great work, though.

Yup there are no traces of TableSortLabel or any sortability in the repo.

It's on the next branch: demos/tables/EnhancedTable.

@oliviertassinari I tried your example however I get this error:

TypeError: Cannot read property 'render' of undefined
EnhancedTable.render
http://localhost:8004/app.a3611250a45594961d8c.js:122073:47
http://localhost:8004/app.a3611250a45594961d8c.js:11761:22
measureLifeCyclePerf
http://localhost:8004/app.a3611250a45594961d8c.js:11040:13
ReactCompositeComponentWrapper._renderValidatedComponentWithoutOwnerOrContext
http://localhost:8004/app.a3611250a45594961d8c.js:11760:26
ReactCompositeComponentWrapper._renderValidatedComponent
http://localhost:8004/app.a3611250a45594961d8c.js:11787:33
ReactCompositeComponentWrapper.performInitialMount
http://localhost:8004/app.a3611250a45594961d8c.js:11327:31
ReactCompositeComponentWrapper.mountComponent
http://localhost:8004/app.a3611250a45594961d8c.js:11223:22
Object.mountComponent
http://localhost:8004/app.a3611250a45594961d8c.js:3816:36
ReactDOMComponent.mountChildren
http://localhost:8004/app.a3611250a45594961d8c.js:10366:45
ReactDOMComponent._createInitialChildren
http://localhost:8004/app.a3611250a45594961d8c.js:7453:33

It's on the next branch: demos/tables/EnhancedTable.

@oliviertassinari Sorry to bother. It seems that when I run npm install material-ui@next in order to install the pre-release package in which this EnhancedTable exists, the TableSortLabel component is missing from the resulting material-ui-build folder. Am I missing a critical step here? Thanks in advance.

@GarrettVD ~The next branch isn't released yet, so you will have to npm install from github.~

Edit: we have since released an early alpha.

@mbrookes Ahh, gotcha. Thanks.

Great job, very interested in this feature. Thanks

@oliviertassinari is sortable and pagination on next branch as well? When is that branch going to be public or merged into current one? Lots of interesting stuff is kept in there :)

@damianobarbati We've released an early alpha: npm install material-ui@next

for anyone else looking for the alpha docs on this: https://material-ui-1dab0.firebaseapp.com/#/component-demos/tables

hi, newbie checking in. sorting tables would be lovely! when will this be merged into the stable production version of MUI?

Waiting for this release to be able to sort data in my table. Great work!

Hi,

I see there is a new release, but I can't seem to find the sorting feature in the source code. Am I missing something?

@nshung You will need to use material-ui@next

https://material-ui-1dab0.firebaseapp.com/component-demos/tables

This is great. I see dynamic sorting in the next demo. Do you think it would make sense to add support for dynamic filtering here? Is that beyond the scope of this component? Would it be better implemented outside MUI?

@lrettig Dynamic sorting was implemented in user space, I think that we should do the same for dynamic filtering. Why? Because it's much easier to cover 80% of the use cases with 20% of the effort that way. We have learned with the master branch that people have a very wide variety of use cases with the tables.

Hi @oliviertassinari thanks for the feedback. I'm a little confused, though--wasn't sorting added here to the next branch? So won't that make it to master in the future? Sorry if I'm missing something obvious.

@lrettig Right, the next branch will eventually going to be merged into the master one.

looking forward for this to go live.

@oliviertassinari -- Thanks! But I'm still confused about your previous comment. You suggested that:

Dynamic sorting was implemented in user space, I think that we should do the same for dynamic filtering

But if sorting is here, in the next branch, and will go to master, doesn't that mean it's _not_ in user space? Am I missing something?

Thanks again.

@lrettig By user space, I mean that it can be implemented outside of Material-UI. That what have been done on the next/v1-alpha branch demo.

I see this is closed. Is this available in a release that is not material-ui@next? I would like to lock down a version for the time being if possible.

@jspengeman @next is an alias, as we are speaking it's targeting v1.0.0-alpha.21.

@oliviertassinari thanks! I understood as much, just saw this was opened in 2015 and was wondering the functionality has made it into a 0.X release at the point. Assuming the answer is no?

How this functionality made it into the release of material-ui?

@oliviertassinari how is it going?

The feature hasn't made it to Material-UI. However, we demonstrate how to implement it in the documentation of v1.

@oliviertassinari thnx for ur fast reply!

Was this page helpful?
0 / 5 - 0 ratings