mui / material-ui

Material UI: Ready-to-use foundational React components, free forever. It includes Material UI, which implements Google's Material Design.
https://mui.com/material-ui/
MIT License
91.86k stars 31.57k forks source link

Sortable Columns in Table #1352

Closed sjstebbins closed 7 years ago

sjstebbins commented 8 years ago

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

jkruder commented 8 years ago

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.

zachguo commented 8 years ago

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

zachguo commented 8 years ago

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.

zachguo commented 8 years ago

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

jkruder commented 8 years ago

@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.

zachguo commented 8 years ago

@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

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

jkruder commented 8 years ago

@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.

zachguo commented 8 years ago

Yup, SortableTable is a good idea.

zachguo commented 8 years ago

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
sjstebbins commented 8 years ago

@zachguo whats the status of this?

zachguo commented 8 years ago

@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.

CumpsD commented 8 years ago

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

Nice work

daniel-sim commented 8 years ago

@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.

shaurya947 commented 8 years ago

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

zachguo commented 8 years ago

@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?

shaurya947 commented 8 years ago

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..

rschwabco commented 8 years ago

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.

zachguo commented 8 years ago

@roieki this is what we did too

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

shaurya947 commented 8 years ago

Does anybody want to take that up?

vorlov commented 8 years ago

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;
  }
}
chrisrittelmeyer commented 8 years ago

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

vorlov commented 8 years ago

@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;
chrisrittelmeyer commented 8 years ago

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

vorlov commented 8 years ago

@chrisrittelmeyer stange question) sure I have)

chrisrittelmeyer commented 8 years ago

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.

JK82 commented 7 years ago

@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?

vorlov commented 7 years ago

@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

JK82 commented 7 years ago

@vorlov Sweet, again this is really nice work

NeXTs commented 7 years ago

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

vorlov commented 7 years ago

@NeXTs what do you mean?

NeXTs commented 7 years ago

@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?

nathanmarks commented 7 years ago

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

vorlov commented 7 years ago

@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.

NeXTs commented 7 years ago

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

nathanmarks commented 7 years ago

@NeXTs

https://t.gyazo.com/teams/nathanmarks/fe8c39995f9cace022168d5b0501a138.mp4

vorlov commented 7 years ago

@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 } } />

NeXTs commented 7 years ago

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

jimmydon commented 7 years ago

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

vorlov commented 7 years ago

@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;
  }
};
jimmydon commented 7 years ago

Thanks for the update.

sibelius commented 7 years ago

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

vorlov commented 7 years ago

@sibelius https://github.com/vorlov/material-ui-sortable-table

sibelius commented 7 years ago

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

remon-nashid commented 7 years ago

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.

oliviertassinari commented 7 years ago

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

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

acpower7 commented 7 years ago

@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
GarrettVD commented 7 years ago

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.

mbrookes commented 7 years ago

@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.

GarrettVD commented 7 years ago

@mbrookes Ahh, gotcha. Thanks.

leialexisjiang commented 7 years ago

Great job, very interested in this feature. Thanks