Skip to content

Instantly share code, notes, and snippets.

@andyhite
Last active January 31, 2017 20:58
Show Gist options
  • Save andyhite/a2ae85d9c4e79910d17e019cb50b470c to your computer and use it in GitHub Desktop.
Save andyhite/a2ae85d9c4e79910d17e019cb50b470c to your computer and use it in GitHub Desktop.
import React from 'react';
import { storiesOf, action } from '@kadira/storybook';
import { Table } from '../src';
const columns = [
{ header: 'Foo', key: 'foo.bar', style: { width: '30%' } },
{ header: 'Bar', key: 'bar', style: { width: '30%' } },
{ header: 'Baz', key: 'baz', style: { width: '40%' } },
];
const rows = [
{ foo: { bar: 'Foo 1' }, bar: 'Bar 1', baz: 'Baz 1', disabled: true, removed: true },
{ foo: { bar: 'Foo 2' }, bar: null, baz: null, error: true },
{ foo: { bar: 'Foo 3' }, bar: '', baz: 'Baz 3'},
{ foo: null, bar: 'Bar 4', baz: 'Baz 4' },
];
storiesOf('Table', module).add('Default', () => {
return (
<Table columns={columns} rows={rows} />
);
}).add('With no rows', () => {
return (
<Table columns={columns} rows={[]} emptyText="There are no rows" />
);
}).add('With a header click action', () => {
return (
<Table
columns={columns.map((column) => (
{ ...column, headerIcon: 'arrow_drop_down' }
))}
rows={rows}
onClickHeader={action('click header')}
/>
);
}).add('With selectable rows', () => {
return (
<Table selectable
columns={columns}
rows={rows}
onSelectRow={action('select row')}
onSelectAll={action('select all')}
/>
);
}).add('With expandable rows', () => {
return (
<Table expandable columns={columns} rows={rows}>
{(row) => (
<div>
Expanded {row.baz}
</div>
)}
</Table>
);
}).add('With fancy expandable rows', () => {
return (
<Table fancy expandable columns={columns} rows={rows}>
{(row) => (
<div>
Expanded {row.baz}
</div>
)}
</Table>
);
});
@import '../../styles/settings';
.root {
display: flex;
flex-direction: column;
margin-bottom: $gutter; position: relative;
z-index: 0;
}
.head {
font-weight: bold;
}
.headerGroup,
.cellGroup {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.header,
.cell {
align-items: center;
display: flex;
flex-grow: 1;
flex-shrink: 1;
padding: $gutter / 2;
&.fixed {
flex-grow: 0;
flex-shrink: 0;
}
&.centered {
justify-content: center;
}
&.removed {
text-decoration: line-through;
}
}
.header {
&.clickable {
cursor: pointer;
&:hover {
color: $dark-green;
}
}
}
.row {
background-color: $white;
border-bottom: 1px solid transparent;
border-top: 1px solid transparent;
display: flex;
flex-direction: column;
min-height: $line-height - 2px;
padding: 0 ($gutter / 2);
.body &:nth-child(odd) {
background-color: $light-green;
}
.expandable .body & {
position: relative;
&:hover {
border-bottom: 1px solid darken($light-green, 10%);
border-top: 1px solid darken($light-green, 10%);
z-index: 1;
}
&.expanded {
border-bottom: 1px solid darken($light-green, 20%);
border-top: 1px solid darken($light-green, 20%);
z-index: 2;
.cellGroup {
border-bottom: 1px solid rgba($dark-green, 0.15);
}
&:hover {
z-index: 2;
}
}
.cellGroup {
cursor: pointer;
}
}
.fancy .body &.expanded {
transform: scale(1.01,1.01);
}
.detail {
padding: $gutter / 2;
}
&.error {
color: $red;
}
}
.errorIcon {
margin-right: $gutter / 2;
}
import React, { Component, PropTypes } from 'react';
import { getStyles } from '../../styles';
import { keypath } from '../../utils';
import CheckBoxInput from '../CheckBoxInput';
import Icon from '../Icon';
const renderValueAtKeypath = (row, { key, renderer }) => {
let value = key ? keypath.get(row, key) : row;
if (renderer) value = renderer.call(null, value, row);
return value || <span dangerouslySetInnerHTML={{ __html: '&mdash;' }} />;
};
const factory = (defaultTheme) => {
class Table extends Component {
state = {
expandedRowIndex: null,
}
handleClickHeader = (header, headerIndex) => {
if (this.props.onClickHeader) {
this.props.onClickHeader(header, headerIndex);
}
}
handleExpandRow = (event, rowIndex) => {
if (event.target.nodeName === "INPUT") return;
const { expandable } = this.props;
const { expandedRowIndex } = this.state;
if (expandable) {
if (rowIndex === expandedRowIndex) {
this.setState({
expandedRowIndex: null,
});
} else {
this.setState({
expandedRowIndex: rowIndex,
});
}
}
}
handleSelectRow = (row, rowIndex) => {
if (this.props.onSelectRow) {
this.props.onSelectRow(row, rowIndex);
}
}
handleDeselectRow = (row, rowIndex) => {
if (this.props.onDeselectRow) {
this.props.onDeselectRow(row, rowIndex);
}
}
handleSelectAll = () => {
if (this.props.onSelectAll) {
this.props.onSelectAll();
}
}
handleDeselectAll = () => {
if (this.props.onDeselectAll) {
this.props.onDeselectAll();
}
}
render() {
const { handleClickHeader, handleExpandRow, handleSelectRow, handleDeselectRow, handleSelectAll, handleDeselectAll } = this;
const { allSelected, children, columns, emptyText, expandable, fancy, onClickHeader, rows, selectable = false, theme } = this.props;
const { expandedRowIndex } = this.state;
const styles = getStyles(defaultTheme, theme, {
root: {
selectable,
expandable,
fancy,
},
header: ({ fixed, clickable }) => ({
fixed,
clickable,
}),
row: ({ error, expanded }) => ({
error,
expanded,
}),
cell: ({ fixed, centered, removed, error }) => ({
fixed,
centered,
removed,
error,
}),
});
return (
<div className={styles.root} data-ui="Table">
<div className={styles.head}>
<div className={styles.row()}>
<div className={styles.headerGroup}>
{selectable && (
<div className={styles.header({ fixed: true })}>
<CheckBoxInput
onChange={allSelected ? handleDeselectAll : handleSelectAll}
checked={allSelected}
/>
</div>
)}
{columns.map((column, i) => (
<div key={i}
className={styles.header({ clickable: !!onClickHeader, fixed: column.fixed })}
onClick={() => handleClickHeader(column, i)}
style={column.style || {}}
>
{column.header}
{column.headerIcon && (
<Icon theme={{ root: styles.headerIcon }} name={column.headerIcon} />
)}
</div>
))}
</div>
</div>
</div>
<div className={styles.body}>
{rows.length > 0 ? (
rows.map((row, i) => (
<div className={styles.row({ expanded: expandedRowIndex === i, error: row.error })} key={i}>
<div className={styles.cellGroup} onClick={(event) => handleExpandRow(event, i)}>
{selectable && (
<div className={styles.cell({ fixed: true })}>
<CheckBoxInput
onChange={() => row.selected ? handleDeselectRow(row, i) : handleSelectRow(row, i)}
checked={!row.disabled && row.selected}
disabled={row.disabled}
/>
</div>
)}
{columns.map((column, ii) => (
<div key={ii}
className={styles.cell({ fixed: column.fixed, removed: row.removed })}
style={column.style || {}}
>
{row.error && ii === 0 && (
<Icon name="error" theme={{ root: styles.errorIcon }} />
)}
{renderValueAtKeypath(row, column)}
</div>
))}
</div>
{expandedRowIndex === i && (
<div className={styles.detail}>
{children.call(this, row)}
</div>
)}
</div>
))
) : (
<div className={styles.row()}>
<div className={styles.cellGroup}>
<div className={styles.cell({ centered: true })}>
{emptyText || 'No data available'}
</div>
</div>
</div>
)}
</div>
</div>
);
}
}
Table.propTypes = {
allSelected: PropTypes.bool,
children: PropTypes.func,
columns: PropTypes.arrayOf(
PropTypes.shape({
header: PropTypes.string,
key: PropTypes.string,
headerIcon: PropTypes.string,
})
).isRequired,
emptyText: PropTypes.node,
expandable: PropTypes.bool,
fancy: PropTypes.bool,
onClickHeader: PropTypes.func,
rows: PropTypes.arrayOf(
PropTypes.object,
).isRequired,
selectable: PropTypes.bool,
theme: PropTypes.object,
};
return Table;
};
export default factory;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment