✨ Features
editable— opt-in, default off. Click a string or number to open an inline editor:Entercommits,Escapecancels, blur commits. Click a boolean and it toggles outright, since there is nothing to type.Enteron a focused row opens that row's editor, so the whole flow works from the keyboard without adding a tab stop per value — a large tree stays tabbable, and a test asserts the cells carry notabindex.onChange(next, change)— receives the new data and a description of what changed.JsonTreestays controlled: it holds no copy of your data, and the change only takes effect once you feed it back throughdata. SettingeditablewithoutonChangelogs a dev-only warning, which is the easy way to get this wrong.editableTypes,isEditable({ path, … })andvalidate({ value, … })— narrow which types are editable, lock individual nodes, and reject a value inline beforeonChangeever fires.pathSegmentson every node — a real address, one step per level, object keys as strings and array indices as numbers. The existingpathis unchanged, soonNodeClick,showPathOnHoverand storedexpandedarrays behave exactly as before.
const [data, setData] = useState(initial);
<JsonTree
data={data}
editable
onChange={(next) => setData(next)}
isEditable={({ path }) => path !== 'root.id'}
validate={({ value }) => (String(value).trim() === '' ? 'Cannot be empty' : null)}
/>⚡ Improvements
- Writes preserve identity. Only the spine down to the target is rebuilt; everything else is carried across by reference, so a
Date,Map,Set,RegExp,BigInt, function or React element elsewhere in the tree survives an edit unchanged. AstructuredCloneor a JSON round-trip would flatten or destroy every one of them — there is a test asserting identity withtoBe, not equality. - A write that cannot resolve throws instead of guessing. An unreachable address means the caller and the tree disagree about the shape of the data, and writing anyway would corrupt it.
- Nodes without an address can never be edited, whatever
editableTypessays: children of aMaporSet(their display keys are synthetic, and aMapkey can be any value at all) and properties of a function expanded withdisplayFunctions="as-object"(they belong to a synthetic object that is not in your data). - Typing does not re-render the tree. The draft value lives inside the editor; only the address of the node being edited is lifted.
🛠️ Other
Two constraints had to be solved before any of this could work, and both are worth knowing if you build on the component:
pathwas a label, not an address. It joins keys with dots, so{ 'a.b': 1 }and{ a: { b: 1 } }both render atroot.a.b, and an array index is indistinguishable from a numeric object key. HencepathSegments.- Mantine's
Treefights inline inputs.TreeNodeowns keyboard navigation on theli[role="treeitem"]and callspreventDefault()on the arrow keys and on Space (expandOnSpacedefaults totrue), so a field inside a node cannot take a space and its caret cannot move. The editor stops pointer and key events before they leave it; the test that types"a b c"is the proof — remove thestopPropagationand it becomes"abc".
116 tests (up from 90), 13 of them for the path setter alone, plus an end-to-end pass in a real browser: the space landed, the caret moved without stealing tree focus, Enter committed, a Date stayed intact across two edits and was never offered for editing, a node locked by isEditable stayed read-only, an empty value was rejected inline without firing onChange, and Escape restored the original.
📝 Summary
Editing values, and only values. Renaming keys, adding, removing and reordering are deliberately out: they all need a path format change — escaping dots, distinguishing index from key — which is a different, larger piece of work rather than more of this one. #22 stays open for it.
What's Changed
Full Changelog: 3.3.0...3.4.0