Asynchronous loading with Controlled selection
This example demonstrates how to create a checkbox tree with asynchronous loading and controlled selection
- Fruits-1
- Drinks-2
- Vegetables-3
import React, { useRef, useState } from "react";
import { FaSquare, FaCheckSquare, FaMinusSquare } from "react-icons/fa";
import { IoMdArrowDropright } from "react-icons/io";
import { AiOutlineLoading } from "react-icons/ai";
import TreeView from "react-accessible-treeview";
import cx from "classnames";
import "./styles.css";
const initialData = [
{
name: "",
id: 0,
children: [1, 2, 3],
parent: null,
},
{
name: "Fruits",
children: [],
id: 1,
parent: 0,
isBranch: true,
},
{
name: "Drinks",
children: [4, 5],
id: 2,
parent: 0,
isBranch: true,
},
{
name: "Vegetables",
children: [],
id: 3,
parent: 0,
isBranch: true,
},
{
name: "Pine colada",
children: [],
id: 4,
parent: 2,
},
{
name: "Water",
children: [],
id: 5,
parent: 2,
},
];
function MultiSelectCheckboxAsyncControlled() {
const loadedAlertElement = useRef(null);
const [data, setData] = useState(initialData);
const [nodesAlreadyLoaded, setNodesAlreadyLoaded] = useState([]);
const [selectedIds, setSelectedIds] = useState([]);
const [selectChildren, setSelectChildren] = useState(false);
const [preserveSelection, setPreserveSelection] = useState(false);
const [manuallySelectedNodes, setManuallySelectiedNodes] = useState([]);
const updateTreeData = (list, id, children) => {
const data = list.map((node) => {
if (node.id === id) {
node.children = children.map((el) => {
return el.id;
});
}
return node;
});
return data.concat(children);
};
const onLoadData = ({ element }) => {
return new Promise((resolve) => {
if (element.children.length > 0) {
resolve();
return;
}
setTimeout(() => {
setData((value) =>
updateTreeData(value, element.id, [
{
name: `Child Node ${value.length}`,
children: [],
id: value.length,
parent: element.id,
isBranch: true,
},
{
name: "Another child Node",
children: [],
id: value.length + 1,
parent: element.id,
},
])
);
if (selectChildren) {
preserveSelection
? setSelectedIds([
...new Set([...manuallySelectedNodes, ...selectedIds]),
data.length,
data.length + 1,
])
: setSelectedIds([data.length, data.length + 1]);
}
resolve();
}, 1000);
});
};
const wrappedOnLoadData = async (props) => {
const nodeHasNoChildData = props.element.children.length === 0;
const nodeHasAlreadyBeenLoaded = nodesAlreadyLoaded.find(
(e) => e.id === props.element.id
);
await onLoadData(props);
if (nodeHasNoChildData && !nodeHasAlreadyBeenLoaded) {
const el = loadedAlertElement.current;
setNodesAlreadyLoaded([...nodesAlreadyLoaded, props.element]);
el && (el.innerHTML = `${props.element.name} loaded`);
// Clearing aria-live region so loaded node alerts no longer appear in DOM
setTimeout(() => {
el && (el.innerHTML = "");
}, 5000);
}
};
const handleNodeSelect = ({ element, isSelected }) => {
isSelected &&
setManuallySelectiedNodes([...manuallySelectedNodes, element.id]);
!isSelected &&
setManuallySelectiedNodes(
manuallySelectedNodes.filter((id) => id === element.id)
);
};
return (
<>
<div>
<div
className="visually-hidden"
ref={loadedAlertElement}
role="alert"
aria-live="polite"
></div>
<button onClick={() => setSelectChildren(!selectChildren)}>
Select next loaded children - [{JSON.stringify(selectChildren)}]
</button>
<button
onClick={() => setPreserveSelection(!preserveSelection)}
style={{ marginLeft: "16px" }}
>
Preserve current selection - [{JSON.stringify(preserveSelection)}]
</button>
<div className="checkbox">
<TreeView
data={data}
aria-label="Checkbox tree"
onLoadData={wrappedOnLoadData}
onNodeSelect={handleNodeSelect}
selectedIds={selectedIds}
multiSelect
propagateSelect
togglableSelect
propagateSelectUpwards
nodeRenderer={({
element,
isBranch,
isExpanded,
isSelected,
isHalfSelected,
getNodeProps,
level,
handleSelect,
handleExpand,
}) => {
const branchNode = (isExpanded, element) => {
return isExpanded && element.children.length === 0 ? (
<>
<span
role="alert"
aria-live="assertive"
className="visually-hidden"
>
loading {element.name}
</span>
<AiOutlineLoading
aria-hidden={true}
className="loading-icon"
/>
</>
) : (
<ArrowIcon isOpen={isExpanded} />
);
};
return (
<div
{...getNodeProps({ onClick: handleExpand })}
style={{ marginLeft: 40 * (level - 1) }}
>
{isBranch && branchNode(isExpanded, element)}
<CheckBoxIcon
className="checkbox-icon"
onClick={(e) => {
!isExpanded && handleExpand(e);
handleSelect(e);
e.stopPropagation();
}}
variant={
isHalfSelected ? "some" : isSelected ? "all" : "none"
}
/>
<span className="name">
{element.name}-{element.id}
</span>
</div>
);
}}
/>
</div>
</div>
</>
);
}
const ArrowIcon = ({ isOpen, className }) => {
const baseClass = "arrow";
const classes = cx(
baseClass,
{ [`${baseClass}--closed`]: !isOpen },
{ [`${baseClass}--open`]: isOpen },
className
);
return <IoMdArrowDropright className={classes} />;
};
const CheckBoxIcon = ({ variant, ...rest }) => {
switch (variant) {
case "all":
return <FaCheckSquare {...rest} />;
case "none":
return <FaSquare {...rest} />;
case "some":
return <FaMinusSquare {...rest} />;
default:
return null;
}
};
export default MultiSelectCheckboxAsyncControlled;
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-icon {
animation: spinner 1.5s linear infinite;
margin-left: 5px;
}
.visually-hidden {
position: absolute;
clip-path: circle(0);
border: 0;
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
width: 1px;
white-space: nowrap;
}