UNPKG

react-lazy-paginated-tree

Version:

Customizable React Tree-View with Lazy Loading and Pagination

322 lines (304 loc) 10.1 kB
// @flow import React, { Component } from 'react'; import deepEquals from 'fast-deep-equal'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import type { Node, TreeNodeProps, TreeNodeState, Event } from '../types'; import { hasChildren, shouldShowMore, isFullyFetched } from '../util'; class TreeNode extends Component<TreeNodeProps, TreeNodeState> { // Each TreeNode contains its own state, but props are the real // source of truth (unless usesLocalState === true) so we need // to ensure that important prop changes (selected, expanded, children) // are also reflected in the local state. static getDerivedStateFromProps( { node, useLocalState }: TreeNodeProps, state: TreeNodeState, ) { const { selected, expanded, children } = state; if ( !useLocalState && (selected !== node.selected || expanded !== node.expanded || !deepEquals(children, node.children)) ) { return { selected: node.selected, expanded: node.expanded, children: node.children, }; } return null; } constructor(props: TreeNodeProps) { super(props); const { node } = props; const { expanded, selected, children, page } = node; this.state = { expanderLoading: false, paginatorLoading: false, expanded, selected, children, page, }; } // PERFORMANCE: only update the node when pertinent component // state changes. This synergizes with getDerivedStateFromProps shouldComponentUpdate(nextProps: TreeNodeProps, nextState: TreeNodeState) { return !deepEquals(this.state, nextState); } // handler for paginating on a list of siblings. Determine if more siblings need to be // loaded and append them to the end of the list. This method is only called // if we are paginating loadMore = async (e: Event, node: Node) => { const { paginated, pageLimit, parse, loadChildren, }: TreeNodeProps = this.props; const state: TreeNodeState = { ...this.state }; if ( !isFullyFetched(node, state.children.length) && paginated && pageLimit ) { state.page += 1; const loadedChildren = await loadChildren(node, pageLimit); state.children = state.children.concat( parse ? parse(loadedChildren) : loadedChildren, ); } this.setState(state); }; onKeyLoadMore = async (e: Event, node: Node): Promise<void> => { if (e.key === 'Enter') { await this.loadMore(e, node); } }; // handler for expanding / collapsing a node. Determine if children need to be // loaded and set expanded state. // fires toggleCallback() prop with event and node toggle = async (e: Event, node: Node): Promise<void> => { const { pageLimit, parse, loadChildren, toggleCallback, paginated, } = this.props; const state: TreeNodeState = { ...this.state }; if ( // nothing is loaded so we should load (state.children.length === 0 && hasChildren(node)) || // we're not paginating and the children aren't fully loaded so let's load them // i.e. when you have nodes that have shared identities (!paginated && state.children.length < node.numChildren) ) { state.page += 1; const loadedChildren = await loadChildren(node, pageLimit); state.children = parse ? parse(loadedChildren) : loadedChildren; } state.expanded = !state.expanded; this.setState(state); if (toggleCallback) { toggleCallback(e, node, state); } }; onKeyToggle = async (e: Event, node: Node): Promise<void> => { if (e.key === 'Enter') { await this.toggle(e, node); } }; // handler for selecting a node. // fires selectCallback() prop with event and node select = (e: Event, node: Node): void => { const { selectCallback } = this.props; const state: TreeNodeState = { ...this.state }; state.selected = !state.selected; this.setState(state); if (selectCallback) { selectCallback(e, node, state); } }; onKeySelect = (e: Event, node: Node): void => { if (e.key === 'Enter') { this.select(e, node); } }; // node "toggle" handler to set expander loading states and prevent // multiple "toggle" actions from being triggered simultaneously. // currently relies on stopPropagation so that toggling doesn't trigger // parent level select handler but this may change in future versions handleToggle = async ( e: Event, node: Node, callable: Function, disabled: boolean, ) => { e.stopPropagation(); if (!disabled) { const { expanded, children }: TreeNodeState = this.state; if (!expanded && children.length === 0) { this.setState({ expanderLoading: true }); await callable(e, node); this.setState({ expanderLoading: false }); } else { await callable(e, node); } } }; // pagination "load more" handler to set paginator loading states and // prevent multiple "load more" actions from being triggered simultaneously. handleLoadMore = async ( e: Event, node: Node, callable: Function, disabled: boolean, ) => { if (!disabled) { this.setState({ paginatorLoading: true }); await callable(e, node); this.setState({ paginatorLoading: false }); } }; // render children if they exist and the node is expanded // ensure that depth is incremented for hierarchical indentation renderChildren() { const { depth, node }: TreeNodeProps = this.props; const { expanded, children }: TreeNodeState = this.state; let childComponents = []; if (expanded && hasChildren(node)) { childComponents = children.map((childNode: Node, index: number) => ( <TreeNode {...this.props} key={childNode.id || index} depth={depth + 1} node={childNode} /> )); } return childComponents; } render() { const { depth, node, theme, indentWidth, List, ListItem, Expander, Checkbox, Body, Paginator, Loading, DepthPadding, paginated, doubleClickSelect, }: TreeNodeProps = this.props; const { expanderLoading, paginatorLoading, expanded, selected, }: TreeNodeState = this.state; const children = this.renderChildren(); //only supports single click OR double click let doubleClickFunction = doubleClickSelect ? e => this.select(e, node) : undefined; let clickFunction = doubleClickSelect ? undefined : e => this.select(e, node); return ( <React.Fragment> {/* ListItem: Overridable container component */} <ListItem theme={theme} node={node} onClick={clickFunction} onDoubleClick={doubleClickFunction} onKeyPress={e => this.onKeySelect(e, node)} > {/* DepthPadding: Overridable Component for hierarchical indentation */} <DepthPadding indentWidth={indentWidth} depth={depth} /> {/* Expander: Overridable Component for toggling expanded/collapsed state */} {hasChildren(node) ? ( <Expander theme={theme} node={node} onClick={e => this.handleToggle(e, node, this.toggle, expanderLoading) } onKeyPress={e => this.handleToggle(e, node, this.onKeyToggle, expanderLoading) } expanded={expanded} /> ) : ( <span style={theme.expanderStyle} /> )} {/* CheckBox: Overridable Component for visualizing selection state */} <Checkbox theme={theme} node={node} selected={selected} /> {/* Body: Overridable node body */} <Body theme={theme} node={node} /> </ListItem> {/* List: Overridable container component for node children */} <List theme={theme}> {/* Animation for node expand / collapse */} <ReactCSSTransitionGroup transitionName="slide" transitionEnterTimeout={200} transitionLeaveTimeout={200} > {/* Loading: Overridable loading bar for pagination */} {expanderLoading && <Loading theme={theme} node={node} />} {children.length > 0 && ( <div> {/* render children here */} {children} {/* Loading: Overridable loading bar for pagination */} {paginatorLoading && ( <Loading theme={theme} node={node} indentWidth={indentWidth} depth={depth} /> )} {/* Paginator: Overridable "load more" pagination button */} {!paginatorLoading && paginated && shouldShowMore(node, children.length) && ( <Paginator theme={theme} node={node} indentWidth={indentWidth} depth={depth} onClick={e => this.handleLoadMore( e, node, this.loadMore, paginatorLoading, ) } onKeyPress={e => this.handleLoadMore( e, node, this.onKeyLoadMore, paginatorLoading, ) } /> )} </div> )} </ReactCSSTransitionGroup> </List> </React.Fragment> ); } } export default TreeNode;