Created
January 13, 2020 01:18
-
-
Save nestoralonso/22d85ff16448a375a681a8dd37b5bb88 to your computer and use it in GitHub Desktop.
Renders a Tree with state (a node renders children nodes that render children nodes...)
This file contains 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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Recursive Component in React</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.11.0/umd/react.development.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.development.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.21.1/babel.min.js"></script> | |
</head> | |
<body> | |
<h1>Recursive Tree With State</h1> | |
<div id="react-app"></div> | |
<script type="text/babel"> | |
class NodeView extends React.Component { | |
constructor(props) { | |
super(props); | |
} | |
handleClick = (e) => { | |
e.stopPropagation(); | |
this.props.onNodeToggle(this.props.path); | |
} | |
handleChecked = (e) => { | |
this.props.onNodeChecked(this.props.path); | |
} | |
/** | |
* if the node contains children, renders a folder icon, if it is expanded renders an openfolder icon | |
* @param {boolean} isParent contains children or not | |
* @param {boolean} isExpanded the expanded state of this node | |
* @return {JSX.Element} the JSX of the rendered icon | |
*/ | |
renderIcon(isParent, isExpanded) { | |
if (isParent && isExpanded) { | |
return ( | |
<span | |
className="glyphicon glyphicon-folder-open" | |
aria-hidden="true"> | |
</span>); | |
} else if (isParent && !isExpanded) { | |
return ( | |
<span | |
className="glyphicon glyphicon-folder-close" | |
aria-hidden="true"> | |
</span>); | |
} | |
return ( | |
<span | |
className="glyphicon glyphicon-file node-view__file-icon" | |
aria-hidden="true"> | |
</span>); | |
} | |
/** | |
* if the node contains children and is collapsed renders a plus icon | |
* @param {boolean} isParent contains children or not | |
* @param {boolean} isExpanded the expanded state of this node | |
* @return {JSX.Element} the JSX of the rendered icon | |
*/ | |
renderArrowIcon(isParent, isExpanded) { | |
if (!isParent) { | |
return null; | |
} | |
if (isExpanded) { | |
return ( | |
<span | |
className="glyphicon glyphicon-minus node-view__folder-ctrl" | |
aria-hidden="true" | |
onClick={this.handleClick} | |
> | |
</span>); | |
} | |
return ( | |
<span | |
className="glyphicon glyphicon-plus node-view__folder-ctrl" | |
aria-hidden="true" | |
onClick={this.handleClick} | |
> | |
</span>); | |
} | |
/** | |
* Renders the children of this element of the source object | |
* @param {object[]} items children objects of the source data structure | |
* @param {string} path array of indexes to get to this node | |
* @return {JSX.Element} a recursive JSX structure that contains children of children | |
*/ | |
renderChildren(children, path) { | |
const { isPathExpanded, isNodeChecked, onNodeToggle, onNodeChecked } = this.props; | |
return ( | |
<ul>{children.map((item, i) => { | |
let newPath = null; | |
if (path) { | |
newPath = [...path, i]; | |
} | |
return ( | |
<NodeView | |
node={item} | |
key={i} | |
path={newPath} | |
checked={isNodeChecked(newPath)} | |
expanded={isPathExpanded(newPath)} | |
isPathExpanded={isPathExpanded} | |
isNodeChecked={isNodeChecked} | |
onNodeToggle={onNodeToggle} | |
onNodeChecked={onNodeChecked} | |
/>); | |
})} | |
</ul>); | |
} | |
render() { | |
const { node, path, expanded, checked } = this.props; | |
const isFolder = node.children && node.children.length > 0; | |
const nodeIcon = this.renderIcon(isFolder, expanded); | |
const arrowIcon = this.renderArrowIcon(isFolder, expanded); | |
return ( | |
<li className="node-view__node"> | |
{arrowIcon} | |
{nodeIcon} | |
<input type="checkbox" className="node-view__check" checked={checked} onChange={this.handleChecked} /> | |
<div className="node-view__label"> | |
{node.label} | |
</div> | |
{isFolder && expanded && this.renderChildren(node.children, path)} | |
</li> | |
); | |
} | |
} | |
class TreeView extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
nodeState: {} | |
}; | |
} | |
/** | |
* returns true if the children of this NodeView are expanded, if there is no key in the state returns true | |
* @param {string} path array of indexes to get to this node | |
* @return {boolean} this branch is expanded or not | |
*/ | |
isPathExpanded = (path) => { | |
const { nodeState } = this.state; | |
// if it is undefined assume that is expanded | |
if (nodeState[path] === undefined) { | |
return true; | |
} | |
return nodeState[path].expanded; | |
} | |
isNodeChecked = (path) => { | |
const { nodeState } = this.state; | |
// if it is undefined assume that is not checked | |
if (nodeState[path] === undefined) { | |
return false; | |
} | |
return nodeState[path].checked; | |
} | |
/** | |
* Toggles the expanded state of a node | |
* @param {string} path array of indexes to get to this node | |
*/ | |
handleExpandToggle = (path) => { | |
const isExpanded = this.isPathExpanded(path); | |
const oldState = this.state.nodeState; | |
const oldNodeState = this.state.nodeState[path] || { checked: false, expanded: true }; | |
const newNodeState = { checked: oldNodeState.checked, expanded: !isExpanded }; | |
this.setState({ | |
nodeState: { ...oldState, [path]: newNodeState } | |
}); | |
} | |
handleCheckToggle = (path) => { | |
const oldState = this.state.nodeState; | |
const oldNodeState = this.state.nodeState[path] || { checked: false, expanded: true }; | |
const newNodeState = { checked: !oldNodeState.checked, expanded: oldNodeState.expanded }; | |
this.setState({ | |
nodeState: { ...oldState, [path]: newNodeState } | |
}); | |
} | |
render() { | |
return ( | |
<ul className="node-view__root" style={this.props.style}> | |
<NodeView | |
node={this.props.root} | |
path={[0]} | |
expanded={this.isPathExpanded([0])} | |
onNodeToggle={this.handleExpandToggle} | |
onNodeChecked={this.handleCheckToggle} | |
isPathExpanded={this.isPathExpanded} | |
isNodeChecked={this.isNodeChecked} | |
/> | |
</ul> | |
); | |
} | |
} | |
var FAKE_BOOKMARKS = { | |
"id": "1", | |
"label": "Bookmarks bar", | |
"children": [ | |
{ | |
"id": "6", | |
"label": "TensorFlow", | |
"url": "http://www.tensorflow.org/" | |
}, | |
{ | |
"id": "96", | |
"label": "Introduction to Deep Learning with Python", | |
"url": "https://www.youtube.com/watch?v=S75EdAcXHKk" | |
}, | |
{ | |
"children": [ | |
{ | |
"id": "8", | |
"label": "What interests reddit?", | |
"url": "http://markallenthornton.com/blog/what-interests-reddit/?utm_content=buffer6ba72&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer" | |
}, | |
{ | |
"id": "215", | |
"label": "Foobaz", | |
"url": "http://www.code-labs.io/", | |
"children": [ | |
{ | |
"id": "9", | |
"label": "NG2", | |
"url": "https://angular.io" | |
}, | |
{ | |
"id": "11", | |
"label": "Fast Refresh", | |
"url": "https://facebook.github.io/react-native/docs/fast-refresh", | |
"children": [{ | |
"id": 56, | |
"label": "Hot Reloading with Time Travel", | |
"url": "https://www.youtube.com/watch?v=xsSnOQynTHs" | |
}, | |
{ | |
"id": 196, | |
"label": "Gource Visualization", | |
"url": "https://www.youtube.com/watch?v=bzLCvFG6WbY" | |
}] | |
} | |
] | |
} | |
], | |
"id": "7", | |
"label": "Dev", | |
} | |
] | |
}; | |
ReactDOM.render( | |
<TreeView root={FAKE_BOOKMARKS} />, document.getElementById('react-app') | |
); | |
</script> | |
<style> | |
.node-view__root { | |
padding: 0; | |
margin: 0; | |
box-sizing: border-box; | |
} | |
.node-view__root ul { | |
padding-left: 1rem; | |
} | |
.node-view__folder-ctrl { | |
cursor: pointer; | |
} | |
.node-view__file-icon { | |
margin-left: 0.8em; | |
} | |
.node-view__node { | |
list-style: none; | |
padding: 0; | |
margin: 0; | |
transition: all 0.5s; | |
} | |
.node-view__node .glyphicon { | |
display: block; | |
float: left; | |
margin-right: 0.4em; | |
} | |
.node-view__label { | |
display: inline; | |
} | |
input.node-view__check { | |
display: inline-block; | |
} | |
</style> | |
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" | |
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment