Skip to content

Instantly share code, notes, and snippets.

@nestoralonso
Created January 13, 2020 01:18
Show Gist options
  • Save nestoralonso/22d85ff16448a375a681a8dd37b5bb88 to your computer and use it in GitHub Desktop.
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...)
<!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