Skip to content

3.4.0

Latest

Choose a tag to compare

@gfazioli gfazioli released this 19 Aug 05:26
· 1 commit to master since this release

✨ Features

  • editable — opt-in, default off. Click a string or number to open an inline editor: Enter commits, Escape cancels, blur commits. Click a boolean and it toggles outright, since there is nothing to type. Enter on 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 no tabindex.
  • onChange(next, change) — receives the new data and a description of what changed. JsonTree stays controlled: it holds no copy of your data, and the change only takes effect once you feed it back through data. Setting editable without onChange logs a dev-only warning, which is the easy way to get this wrong.
  • editableTypes, isEditable({ path, … }) and validate({ value, … }) — narrow which types are editable, lock individual nodes, and reject a value inline before onChange ever fires.
  • pathSegments on every node — a real address, one step per level, object keys as strings and array indices as numbers. The existing path is unchanged, so onNodeClick, showPathOnHover and stored expanded arrays 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. A structuredClone or a JSON round-trip would flatten or destroy every one of them — there is a test asserting identity with toBe, 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 editableTypes says: children of a Map or Set (their display keys are synthetic, and a Map key can be any value at all) and properties of a function expanded with displayFunctions="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:

  • path was a label, not an address. It joins keys with dots, so { 'a.b': 1 } and { a: { b: 1 } } both render at root.a.b, and an array index is indistinguishable from a numeric object key. Hence pathSegments.
  • Mantine's Tree fights inline inputs. TreeNode owns keyboard navigation on the li[role="treeitem"] and calls preventDefault() on the arrow keys and on Space (expandOnSpace defaults to true), 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 the stopPropagation and 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