Created
October 15, 2018 14:30
-
-
Save bryzettler/156e6194d7b19d62c6ecfc34c6aaa45e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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