Skip to content

Instantly share code, notes, and snippets.

@bryzettler
Created October 15, 2018 14:30
Show Gist options
  • Save bryzettler/156e6194d7b19d62c6ecfc34c6aaa45e to your computer and use it in GitHub Desktop.
Save bryzettler/156e6194d7b19d62c6ecfc34c6aaa45e to your computer and use it in GitHub Desktop.
// @flow
import * as React from 'react';
import {
get as _get,
throttle as _throttle,
orderBy as _orderBy,
} from 'lodash';
import { Table, Icon } from 'semantic-ui-react';
import EmptyListPlaceholder from '../emptyListPlaceholder/EmptyListPlaceholder';
import styles from './ModelList.scss';
import { PLACEHOLDERS } from '../../constants';
type Props = {
inverted?: boolean,
modelType: string,
models: Array<any>,
tableRowNumbers?: boolean,
tableHeaders: Array<{ header: string, props?: Object}>,
tableCells: Array<{ modelKey: string, props?: Object}>,
tableCellActions?: Array<React.Node> | ?(*) => React.Node | ?(*) => Array<React.Node>,
tableFooterAction?: React.Node,
onRowClick?: Function,
sortable?: boolean,
fixed?: boolean,
singleLine?: boolean,
} & InfiniteScrollProps;
type InfiniteScrollProps = {
infiniteScroll?: boolean,
onInfiniteLoad?: Function,
shouldInfiniteLoad?: boolean,
}
type State = {
isLoading: boolean,
perservedScrollState: number,
column: ?string,
direction: ?string,
}
class ModelList extends React.Component<Props & InfiniteScrollProps, State> {
props: Props;
state = {
isLoading: false,
perservedScrollState: 0,
column: null,
direction: null,
}
componentWillReceiveProps() {
this.setState({
isLoading: false,
});
}
componentDidUpdate(prevProps: Props & InfiniteScrollProps, prevState: State) {
const { ref } = this;
if (ref) {
const tBody = ref.querySelector('tbody');
if (prevState.isLoading && !this.state.isLoading && tBody) {
tBody.scrollTop = this.state.perservedScrollState;
}
}
}
ref = null;
getModelKey = (model: Object, key: string) => (
_get(model, key)
);
handleInfiniteLoad = () => {
this.setState({ isLoading: true });
if (this.props.onInfiniteLoad) {
this.props.onInfiniteLoad();
}
}
setPerservedScrollState = _throttle(scrollTop => (
this.setState({ perservedScrollState: scrollTop })
), 500)
handleScroll = () => {
const {
ref,
state,
props: { infiniteScroll, shouldInfiniteLoad },
} = this;
if (shouldInfiniteLoad && ref) {
const tBody = ref.querySelector('tbody');
const { offsetHeight, scrollTop, scrollHeight } = tBody;
this.setPerservedScrollState(scrollTop);
if (infiniteScroll && !state.isLoading) {
const height = (offsetHeight + scrollTop);
if (height >= (scrollHeight - 150)) {
this.handleInfiniteLoad();
}
}
}
};
refHandler = (domElement: any) => {
this.ref = domElement;
}
handleSort = (clickedColumn: string) => () => {
const {
state: {
column,
direction,
},
} = this;
if (column !== clickedColumn) {
this.setState({
column: clickedColumn,
direction: 'asc',
});
} else {
this.setState({
direction: direction === 'asc' ? 'desc' : 'asc',
});
}
}
generateHeaders = () => {
const {
props,
state: { column, direction },
} = this;
const indicator = (direction && {
asc: 'ascending',
desc: 'descending',
}[direction]);
return (
<Table.Header
className={styles.tHead}
fullWidth
>
<Table.Row>
{props.tableRowNumbers && (<Table.HeaderCell width="1" />)}
{props.tableHeaders.map(
({ header, props: headerProps }, index) => (
<Table.HeaderCell
key={header}
{...headerProps}
sorted={column === props.tableCells[index].modelKey ? indicator : null}
onClick={this.handleSort(props.tableCells[index].modelKey)}
>
{header}
</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>
);
}
generateBody = () => {
const {
state: { column, direction },
props,
} = this;
return (
<Table.Body
className={styles.tbody}
onScroll={this.handleScroll}
>
{_orderBy(props.models, [column], [direction]).map((model: Object, index: number) => (
<Table.Row
key={`model${index + 1}`}
onClick={e => (props.onRowClick && props.onRowClick(e, model))}
>
{props.tableRowNumbers && (
<Table.Cell width="1">
{index + 1}
</Table.Cell>
)}
{props.tableCells.reduce(
(acc, { modelKey, props: cellProps }, idx, orgArray) => {
if (typeof this.getModelKey(model, modelKey) === 'boolean') {
return ([
...acc,
(
<Table.Cell
key={modelKey}
{...cellProps}
>
<Icon
disabled={!this.getModelKey(model, modelKey)}
color={this.getModelKey(model, modelKey) ? 'green' : 'grey'}
name={this.getModelKey(model, modelKey) ? 'checkmark' : 'minus'}
/>
{props.tableCellActions && idx === (orgArray.length - 1) && (
typeof props.tableCellActions === 'function' ?
props.tableCellActions(model) :
props.tableCellActions
)}
</Table.Cell>
),
]);
}
return ([
...acc,
(
<Table.Cell
key={modelKey}
disabled={!this.getModelKey(model, modelKey)}
{...cellProps}
>
{this.getModelKey(model, modelKey) || PLACEHOLDERS.nullValue}
{props.tableCellActions && idx === (orgArray.length - 1) && (
typeof props.tableCellActions === 'function' ?
props.tableCellActions(model) :
props.tableCellActions
)}
</Table.Cell>
),
]);
}, [])}
</Table.Row>
))}
{this.state.isLoading && (
<Table.Row>
<Table.Cell colSpan={3}>
<Icon loading name="spinner" />{`Loading More ${props.modelType}...`}
</Table.Cell>
</Table.Row>
)}
</Table.Body>
);
}
generateFooter = () => {
const { props } = this;
if (props.tableFooterAction) {
return (
<Table.Footer fullWidth>
<Table.Row>
<Table.HeaderCell>
{props.tableFooterAction}
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
);
}
return null;
}
render() {
const { props } = this;
if (!props.models.length) {
return (
<EmptyListPlaceholder message={`No ${props.modelType} found.`} />
);
}
return (
<div
className={styles.wrapper}
ref={this.refHandler}
>
<Table
basic="very"
striped
stackable
size="small"
selectable={!!props.onRowClick}
inverted={!!props.inverted}
fixed={!!props.fixed}
singleLine={!!props.singleLine}
className={[
styles.table,
(props.onRowClick && styles.tableClickable),
].filter(Boolean).join(' ')}
sortable={!!props.sortable}
definition={!!props.tableRowNumbers}
>
{this.generateHeaders()}
{this.generateBody()}
{this.generateFooter()}
</Table>
</div>
);
}
}
export default ModelList;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment