// App.js (React, CodeSandbox-ready)
// Nested checkboxes with parent/child sync and indeterminate states.
import React from "react";
// ----- Sample tree data -----
const tree = {
id: "a",
label: "A",
children: [
{ id: "a1", label: "A1" },
{
id: "a2",
label: "A2",
children: [
{ id: "a21", label: "A2.1" },
{ id: "a22", label: "A2.2" },
],
},
],
};
// ----- Utilities -----
function visit(node, fn) {
fn(node);
node.children?.forEach((c) => visit(c, fn));
}
function initState(root) {
const s = {};
visit(root, (n) => {
s[n.id] = { checked: false, indeterminate: false };
});
return s;
}
function setSubtree(node, checked, state) {
state[node.id] = { checked, indeterminate: false };
node.children?.forEach((child) => setSubtree(child, checked, state));
}
function findParent(root, targetId) {
let parent = undefined;
function dfs(cur) {
for (const child of cur.children ?? []) {
if (child.id === targetId) {
parent = cur;
return true;
}
if (dfs(child)) return true;
}
return false;
}
dfs(root);
return parent;
}
function computeStateFromChildren(children, state) {
const statuses = children.map((c) => state[c.id]);
const allChecked = statuses.every((s) => s.checked && !s.indeterminate);
const noneChecked = statuses.every((s) => !s.checked && !s.indeterminate);
return {
checked: allChecked,
indeterminate: !allChecked && !noneChecked,
};
}
// ----- Components -----
function TreeNode({ node, root, state, setState, level = 0 }) {
const checkboxRef = React.useRef(null);
const s = state[node.id];
React.useEffect(() => {
if (checkboxRef.current)
checkboxRef.current.indeterminate = s.indeterminate;
}, [s.indeterminate]);
const onToggle = (checked) => {
setState((prev) => {
const next = { ...prev };
// Update this subtree
setSubtree(node, checked, next);
// Recompute ancestors up to root
let cur = node;
while (true) {
const parent = findParent(root, cur.id);
if (!parent) break;
next[parent.id] = computeStateFromChildren(parent.children ?? [], next);
cur = parent;
}
return next;
});
};
return (
<div style={{ paddingLeft: level ? 16 : 0 }}>
<label style={{ display: "inline-flex", gap: 8, alignItems: "center" }}>
<input
ref={checkboxRef}
type="checkbox"
checked={s.checked}
aria-checked={
s.indeterminate ? "mixed" : s.checked ? "true" : "false"
}
onChange={(e) => onToggle(e.target.checked)}
/>
{node.label}
</label>
{node.children?.length ? (
<div role="group" style={{ marginTop: 4 }}>
{node.children.map((child) => (
<TreeNode
key={child.id}
node={child}
root={root}
state={state}
setState={setState}
level={level + 1}
/>
))}
</div>
) : null}
</div>
);
}
export default function App() {
const [state, setState] = React.useState(() => initState(tree));
return (
<div style={{ fontFamily: "sans-serif", padding: 16 }}>
<h3>Nested Checkboxes</h3>
<TreeNode node={tree} root={tree} state={state} setState={setState} />
<pre style={{ background: "#f6f8fa", padding: 8, marginTop: 16 }}>
{JSON.stringify(state, null, 2)}
</pre>
</div>
);
}
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)