json-edit-react
June 29, 2026 · View on GitHub
2.0.0-beta.8
- A synchronous
onUpdaterejection (false,{ error }, or a thrown error) now resolves in place rather than applying optimistically and then reverting. The rejected value is never written throughsetData, so it no longer flashes in the editor and leaves nothing transient for downstream state (an undo history, autosave, a dirty flag) to pick up. Asynchronous rejections are unchanged — they still commit optimistically and revert when the promise settles (#391). - As a result, the
onEditEventlifecycle for a synchronously-rejected edit / rename / add is nowsubmit* → cancel* → updateError(nocommit*), matching how the instant operations (delete, move) already report a fast rejection.
2.0.0-beta.7
- A custom node definition's
defaultValuecan now be a function(nodeData) => value, called each time a node is switched to that type — the same callback form the editor-leveldefaultValueprop already supports. Use it for a fresh value (e.g.() => new Date()) instead of one fixed when the module loads. Plain values keep working unchanged.
2.0.0-beta.6
- Renaming a property key now requires
allowDeleteon the node andallowAddon its parent collection — no longer the node's ownallowAdd, andallowEditno longer applies; consistent with how a drag relocate is gated (#374). See the migration guide.
2.0.0-beta.5
- Drag-and-drop now respects the
allow*permissions consistently: reordering within a collection needs itsallowEdit, while relocating into a different collection needsallowDeleteon the source andallowAddon the destination. Picking a node up needs onlyallowDrag(no longerallowDeletetoo). See the migration guide.
2.0.0-beta.4
- Collection custom components with
showOnEditnow receive live child rows while editing (#384). - Restore the
collectionInnerthemeable element (#383).
2.0.0-beta.0
Major Changes
-
6b76705: Renamed the
restrict*props toallow*(inverting their polarity), plus a batch of display-prop renames for naming consistency.restrict*→allow*(semantics inverted).restrictEdit/restrictDelete/restrictAdd/restrictTypeSelection/restrictDragbecomeallowEdit/allowDelete/allowAdd/allowTypeSelection/allowDrag. The polarity flips: abooleaninverts, and aFilterFunctionnow returnstrueto permit a node (it previously returnedtrueto block it). Defaults flip accordingly —allowEdit/allowDelete/allowAdd/allowTypeSelectiondefault totrue, andallowDragdefaults tofalse(drag is still off by default). The axes remain independent:allowEdit={false}does not also disable add/delete/drag — useJsonViewerfor a fully read-only display.Display / config renames (pure renames, no behaviour change unless noted):
keySort→sortKeysrootFontSize→baseFontSizeerrorMessageTimeout→errorDisplayTimestringTruncate→stringTruncateLength(also thecomponentPropsofHyperlink/EnhancedLinkin@json-edit-react/components)showArrayIndices→showArrayIndexesarrayIndexFromOne(boolean) →arrayIndexStart(number):arrayIndexFromOne={true}becomesarrayIndexStart={1}(default0)
The
editorRefimperative API is unchanged:overrideRestrictionsand the'RESTRICTED'startEditresult keep their names.See the migration guide for full mapping tables and recipes.
-
de1cd5d: The clickable icon controls — the ✓ / ✗ confirm/cancel pair and the edit/copy/delete/add icons — are now real
<button>elements instead of<div onClick>, so assistive tech announces them as actionable and reads anaria-label(always present, independent ofshowIconTooltips). Their appearance is unchanged (the default button chrome is reset in the bundled CSS) and they carrytabIndex={-1}, so the editor's field-to-field Tab navigation is unaffected. Two new localisation keys (TOOLTIP_OK,TOOLTIP_CANCEL) provide the confirm/cancel labels.Breaking only for custom CSS that targets these controls by tag name: a selector like
.jer-confirm-buttons > divmust become.jer-confirm-buttons > button. Wrapper-class and icon selectors are unaffected, and consumer-suppliedcustomButtonsremain<div>-wrapped. -
b844e0f: The
collapseprop now defaults to3(previouslyfalse). The tree opens with its top three levels of nesting expanded and deeper nodes collapsed, so deeply-nested data no longer renders its full depth on first paint. Data that's three levels or shallower is unaffected (still fully expanded). To restore the always-fully-open behaviour, passcollapse={false}. -
13f5950: Fix type-switching away from a custom node mid-edit leaving an inconsistent editing UI (#335).
Switching a custom node's type to a standard one now behaves like any other deferred primitive type change: the custom component yields to the target type's standard editor, pre-filled with a conversion of the node's actual value, and the single commit happens on confirm. Previously the custom component kept rendering over the editor while the buffer was silently replaced with the
DEFAULT_STRINGplaceholder ('New data!'), which confirm then committed verbatim.Conversions are also safe for non-JSON sources:
null/undefined→stringyields an empty buffer (not the literal"null"/"undefined"), aSymbolconverts to its description forstringand to0fornumber(where it previously threw), andNaN→numberyields0.Breaking: the
DEFAULT_STRINGlocalisation key is removed — the placeholder it supplied no longer exists. Remove it from yourtranslationsobject if present. -
b82f8db: Renamed the
CustomNodeDefinitionfields and props type for consistency, around one distinction: a node is a position in the data tree; a component is the React function that renders it.- Render slots (they hold React components, not "elements"):
element→component,customKey→keyComponent,wrapperElement→wrapperComponent. - Config:
customNodeProps→componentProps(the bag passed tocomponent+keyComponent). - Visibility flags (now all positive
show*):hideKey→showKey(polarity inverted —showKeydefaults totrue),showInTypesSelector→showInTypeSelector. - Types:
CustomNodeProps→CustomComponentProps(the props your component receives; also resolves the long-standingCustomNodeProps/CustomNodeDefinitionname clash). The newCustomWrapperPropstypeswrapperComponent, which now receives its config aswrapperProps(previously delivered ascustomNodeProps).CustomNodeDefinitionandCustomKeyPropskeep their names. - Error reporter removed: custom components no longer receive an error-reporter prop (v1's positional
onError). To reject invalid input,throwfrom the definition'sfromStandardType— the editor rejects the commit, keeps the editor open, shows the message inline, and fires the consumer'sonError. This removes the name clash with the editor-levelonErrorobserver. - Key component:
CustomKeyProps.setIsEditingKey→startEditingKey— a zero-arg "enter key-edit mode" command, renamed off thesetIs*prefix that wrongly implied a ReactDispatchsetter. - Keyboard handler:
CustomComponentProps.handleKeyPress→onKeyDown— "keyPress" is React's deprecated event name;onKeyDownmatchesTextEditorProps.onKeyDown(and the publicAutogrowTextArea's handler prop). - Value access:
CustomComponentPropsno longer carries the redundantdatafield (it duplicatedvalue, and was typedunknown). Read the node's live value viavalue, or its committed value vianodeData.value.
All 12 components in
@json-edit-react/componentsuse the new field names. Consumers overriding a shipped definition'scustomNodePropsmust rename tocomponentProps, and custom-component bodies must rename the props type (CustomNodeProps→CustomComponentProps), the config prop they destructure (customNodeProps→componentProps), move any error-reporting call (v1'sonError) into athrowingfromStandardType, rename a key component'ssetIsEditingKeycall tostartEditingKey, rename the key-down handlerhandleKeyPress→onKeyDown, and read the node value viavalue/nodeData.valueinstead ofdata.See the migration guide for the full mapping and before/after examples.
- Render slots (they hold React components, not "elements"):
-
1ac80d0: Fine-grained editing re-renders + React 18 requirement.
- Breaking: the React peer dependency is now
>=18.0.0(was>=16.0.0). v2 uses React's built-inuseSyncExternalStore. - Editing state moved to a selectable external store. Previously every node subscribed to a single editing context, so starting/moving an edit re-rendered the whole tree. Each node now subscribes only to its own editing slice, so moving an edit between nodes re-renders just the nodes involved — a large win on big documents. No public API or behaviour change.
- Drag-while-editing is now blocked at drag-start (reading editing state imperatively) rather than by disabling
draggableon every node, so starting/ending an edit no longer re-renders all draggable nodes.
- Breaking: the React peer dependency is now
-
556b1cf: Replace the
externalTriggersprop with an imperativeeditorRefhandle (#251).The
externalTriggersstate-as-RPC prop is removed, along with theExternalTriggersandEditStatetypes. Imperative control now goes through a typed ref handle attached via a neweditorRefprop. The handle is UI-interactions only — it opens/commits/cancels a value-edit session or collapses nodes; it has no data mutators (you owndata/setData, so mutating data is justsetData(newData)):const editorRef = useRef<JsonEditorHandle>(null) // ... <JsonEditor data={data} setData={setData} editorRef={editorRef} /> editorRef.current.collapse({ path, collapsed, includeChildren }) editorRef.current.startEdit({ path }) // open the value editor editorRef.current.startEdit({ path, overrideRestrictions: true }) // bypass restrictEdit editorRef.current.confirm() // commit the open session editorRef.current.cancel() // discard iteditorRefis a plain ref-valued prop (not therefattribute), soJsonEditor<T>stays generic with full type inference.startEditis synchronous and returns aStartEditResult—trueif it opened the session, else'PATH_NOT_FOUND'(the path is gone) or'RESTRICTED'(restrictEditblocks it). It respectsrestrictEditby default (evaluated at call time); passoverrideRestrictions: trueto bypass it (skips only the filter — youronUpdatestill runs atconfirm()).confirm()commits the open session throughonUpdate(the same path as clicking the editor's confirm button);cancel()discards it.startEditauto-reveals a target collapsed below the current view.JsonVieweracceptseditorReftoo, but itsJsonViewerHandleis collapse-only. Adds theJsonEditorHandle,JsonViewerHandle,StartEditOptions, andStartEditResulttypes to the public API, and exports thesplitPropertyStringpath-parsing helper (companion totoPathString) for building handle paths.Imperative session openers for key-rename and add (
startRename/startAdd) and an awaitableconfirm()returning aCommandResultwere prototyped during the §17 API work but deferred to a later 2.x release (they were the largest removable slice of the §17 bundle growth); the rename/add session events still fire viaonEditEventfor UI-driven sessions. -
2c937a0:
JsonEditoris now generic on the data type.JsonEditor<T = JsonData>— consumers can preserve their data shape across the component boundary:<JsonEditor<MyShape> data={...} setData={...} />.- The generic flows through
data,setData, and the root data slots ofUpdateFunction,OnChangeFunction,OnErrorFunction, plusNodeData.fullDatainside everyFilterFunctionvariant. - Default of
JsonDatakeeps existing untyped code source-compatible. Per-nodevalueandparentDataslots stay wide (they are arbitrary-depth slices, no static type can describe them). - Breaking (json-edit-react v2) only because the emitted
.d.tssignatures change. Runtime behaviour is unchanged.
See the migration guide for details and examples.
-
fca0b35: Split custom components into a separate publishable package.
- New package:
@json-edit-react/componentsships 12 ready-to-use custom node components:Hyperlink,EnhancedLink,DatePicker,DateObject,ColorPicker,Markdown,Image,BooleanToggle,BigInt,NaN,Symbol,Undefined. Heavy third-party libraries (react-datepicker,react-markdown,react-colorful) are bundled as regular dependencies but loaded lazily at runtime viaReact.lazy, so unused components contribute zero to the consumer's bundle. - Breaking (json-edit-react v2): the old
LinkCustomComponentandLinkCustomNodeDefinitionare no longer exported fromjson-edit-react. Replaced byLinkCustomComponent+ thehyperlinkDefinitiondefinition factory (functionally a superset, with configurablecomponentProps) from@json-edit-react/components. Migration:import { hyperlinkDefinition } from '@json-edit-react/components'and passhyperlinkDefinition()tocustomNodeDefinitions. - The
custom-component-libraryworkspace is now a downstream consumer of@json-edit-react/components— itscomponents/folder moved into the new package; its app imports from@json-edit-react/componentslike any other consumer would.
- New package:
-
fca0b35: Split themes into a separate publishable package.
- New package:
@json-edit-react/themesships the six pre-built themes (githubDarkTheme,githubLightTheme,monoDarkTheme,monoLightTheme,candyWrapperTheme,psychedelicTheme). - Breaking (json-edit-react v2): these theme exports are no longer re-exported from
json-edit-react. Consumers mustimport { ... } from '@json-edit-react/themes'. - Also promoted as public API in core (additive, non-breaking among the v2 changes):
AutogrowTextArea(joins existingStringDisplay,StringEdit,toPathString).
- New package:
-
ceb8dd9: Split the
onUpdateoverride return into node-level{ value }and whole-document{ data }.Returning
{ value: X }now replaces the edited node's value (applied at its path), not the whole document — the common case of tidying what the user just entered (lower-case, round, trim, sort this array). It is honoured foreditandadd; forrename/move/deleteit has no target value and is ignored. To replace the whole document (stamp a top-level field, sort siblings, canonicalise the structure), return the new key{ data: X }, which works on every event.Breaking. Previously
{ value: X }replaced the whole document. AnyonUpdaterelying on that — including whole-document timestamp/sort transforms — must switch to{ data: X }. Returning both keys is a mistake:{ data }wins,{ value }is ignored, and a console warning is emitted. See the migration guide (§9). -
b26c2cd: Reworked the editing/commit lifecycle to be optimistic by default, with an optional synchronous gate, and renamed/extended the
onEditEventstream.Optimistic commits. When the user submits an edit, the editor now closes and the data updates immediately; the consumer's
onUpdateruns in the background, and a rejection (false/{ error }/ a thrown error / a rejected promise) automatically reverts the change and surfaces the error. A slowonUpdate(e.g. a remote save) no longer blocks the editor. Each in-flight commit is tracked with its own token, so a late failure reverts only its own node and can't clobber a newer edit.Gating via
hold().onUpdatereceives a second argument,{ hold }. Callinghold()(synchronously, before the firstawait) keeps the editor open and blocks the rest of the tree until the returnedrelease()is called — the path for confirmation dialogs or pre-commit validation. Without it, commits stay optimistic.onEditEventlifecycle. The committed-phase events are renamedconfirm*→commit*(commitEdit/commitRename/commitAdd); a newsubmit*event fires when the user commits (the window ahold()gate runs in); andupdateSuccess/updateErrorreport the background settlement of any committed change whoseonUpdateran. A session is nowstart* → [submit*] → commit*, orstart* → cancel*. A no-op confirm reportscommitEdit(notcancelEdit).See the migration guide (§9, §10) for details.
-
941a1cd: Path identity is now
CollectionKey[]everywhere instead of a dot-joined string.- The internal editing-state, drag-source, and
areChildrenBeingEditedchecks all compare arrays directly, fixing two classes of bug at once:- Keys containing
.no longer collide with deeper paths (e.g.['foo.bar', 'baz']is now distinguishable from['foo', 'bar', 'baz']). - The "is a descendant" check is a proper array prefix, not a string substring — so editing
foobarno longer claimsfoo's children are editing, and draggingfoono longer hides the drop highlight onfoobar.
- Keys containing
toPathStringis still exported, but its encoding changes to/-joinedencodeURIComponent(e.g.['data', 0, 'name']→'data/0/name'). The result is now provably injective. The optional secondkey?: 'key_'argument is removed — the new identity model encodes value-vs-key mode as a field, not a string prefix. If you only usetoPathString's output as an HTMLname/id, no code change is needed.
See the migration guide for details.
- The internal editing-state, drag-source, and
-
a186a61: Theme
stylesgains two row-level themeable elements —headerRow(a collection's header line) andvalueRow(a leaf value's row) — so row height, background, and the like can be themed (e.g.headerRow: { minHeight: '2em' }). ThecollectionInnerelement is removed: its only distinct use — styling the children body apart from the header — is now covered byheaderRow+collection.collection,collectionElement, anddropZoneare unchanged. -
355b7f8:
JsonEditoris now strictly controlled.setDatais required, the controlled/uncontrolled dual mode is gone, and theviewOnlyshorthand is removed. A new sibling exportJsonVieweris the canonical read-only entry point.<JsonEditor>requiressetData— forgetting it is now a TypeScript error rather than a silent "edits don't propagate" footgun.<JsonViewer>(new) wrapsJsonEditorwith all edit affordances locked off. Accepts the same display, theming, keyboard, search, collapse, custom-node, and localisation props but omitssetData, the update callbacks, the edit-permission props, andexternalTriggers.viewOnlyprop is removed. For static read-only displays, use<JsonViewer>. For dynamic permissions-style toggling on the same mounted component, useallowEdit={cond}+allowAdd={cond}+allowDelete={cond}(andallowDrag={cond}if you'd previously enabled drag-and-drop; otherwise the default is alreadyfalse/off).- The internal
useDatahook is deleted —JsonEditornow readsdataandsetDatafrom props directly.
See the migration guide for migration recipes.
-
ece6d70: Replace the v1
enableClipboardprop withshowClipboardButton(boolean, defaulttrue) plus the separateonCopyobserver.enableClipboarddid two unrelated jobs through aboolean | CopyFunctionoverload: toggling the copy button and observing copies. These are now two single-purpose props.showClipboardButtonis a plain display toggle — it sits in theshow*family (showArrayIndexes,showStringQuotes, …), not theallow*capability gates, because hiding the copy button can't actually prevent copying (the value is selectable in the DOM); it only controls whether the convenience button renders. The copy callback moves toonCopy?: OnCopyFunction, which receives the same flatNodeDatapayload every other observer gets, andCopyFunctionis removed in favour ofOnCopyFunction. See the migration guide. -
f9458fc: Rework the theming engine: compose multiple style functions and tidy the theme types. The common cases — passing colours, style objects, arrays, and style functions via the
themeprop — are unchanged.Style functions compose. When an element's value is an array with more than one style function, all of them now run and merge (later wins per property) — matching what the docs always described. Previously only the last function in the array took effect. Functions are still applied after static styles.
Types.
ThemeStylesis nowPartial<Record<ThemeableElement, …>>— inherently optional per key. The compiled style map is partial too, butgetStylesfills any gap with{}, so its public return contract is unchanged.Internally the compile step is now a single pure pass with no behaviour change for existing themes. See the migration guide.
-
a186a61: Themes now own their icon glyphs. The standalone
iconsprop is removed; supply glyphs viatheme.icons(keyedadd/edit/delete/copy/ok/cancel/collection), where each value is anIconDefinition(contentplus optionalviewBox/svgProps/scale). User-supplied glyphs are themeable viacurrentColor, just like the built-ins. The expand/collapse key is renamedchevron→collection. TheIconAdd…IconChevroncomponents,IconProps, andIconReplacementsare no longer exported (the built-in glyphs now live ondefaultTheme.icons);IconDefinition,ThemeIcons, andIconSvg(the glyph renderer — pass anIconDefinition's parts) are added.
Minor Changes
-
94e5598: Opening an edit on another node now commits the in-progress edit instead of cancelling it, matching Tab.
Previously, clicking another node's edit control (its pencil, double-clicking another value, or clicking another key to rename) while an edit was open silently discarded the in-progress buffer and fired
cancelEdit/cancelRename. Tab already committed-then-moved, so the two "leave this field and go edit elsewhere" gestures behaved oppositely. Now a displace behaves like Tab: a changed edit commits (andonUpdateruns), an unchanged one closes viacommit*with noonUpdate/setData, and an edit that can't commit (malformed JSON in a collection edit, a duplicate key in a rename, or a custom component'sfromStandardTypethrowing) blocks the switch — the editor stays open with its inline error, and Esc / ✗ remain the explicit discard path.The one exception is the object add session (typing a new key): a displace still cancels it, since you can't Tab out of a new-key edit either.
This changes the
onEditEventstream for a displaced session fromcancel*tosubmit*/commit*(or nothing extra, for a blocked switch). See the migration guide. -
ae66784: Expose an
isPendingprop on custom node components (CustomComponentProps/CustomWrapperProps). It'struewhile a node's optimistic edit is settling — the value is already applied locally but the consumer's asynconUpdatehasn't resolved yet — andfalseotherwise (including when there's noonUpdate, where the commit settles synchronously). Use it to show a saving/pending state, e.g. a spinner or overlay, for the duration of an async update. See the new "Pending overlay" example in the demo. -
ffb32b3: Allow any
keyboardControlsbinding to be disabled by setting it tonull. A disabled binding is no longer intercepted and falls through to its native browser behaviour — e.g.{ tabForward: null, tabBack: null }restores normal Tab/Shift-Tab focus traversal instead of moving between editable nodes. -
7cb6ba7: Add an
editOnTypeSwitchfield toCustomNodeDefinition(defaultfalse; requirescomponent+showOnEdit): switching to the custom type in the Type selector becomes a local, deferred switch instead of an instant commit — the edit buffer is seeded from the node's current value (viafromStandardType, falling back todefaultValue), the target definition's component renders in edit state, a single commit happens on confirm, and Esc cancels the whole switch. The new collection mounts expanded when the committed value is an object/array, matching the instant-commit path. -
ee583bc: Add dedicated
ERROR_RENAME/ERROR_MOVElocalisation keys andRENAME_ERROR/MOVE_ERRORerror codes for rejected rename/move operations (#308).A rejected rename/move (
onUpdatereturningfalse) now surfaces an operation-specific message ("Rename unsuccessful" / "Move unsuccessful") and a matchingonErrorcode (RENAME_ERROR/MOVE_ERROR), giving these first-class events full parity withadd/delete. Previously both reused the genericERROR_UPDATEmessage andUPDATE_ERRORcode. The two new codes are additive members of the publicJerErrorCodeunion. -
fc23b40: Changing a value's type to
objectorarraynow launches the new collection expanded (#217). A level-basedcollapsesetting would previously leave the just-created collection collapsed, hiding its contents until manually expanded. -
1cb7dc7: Export the
CustomButtonDefinitiontype from the package entry.The
customButtonsprop has always been typed asCustomButtonDefinition[], but the type itself wasn't exported, so TypeScript consumers couldn't import it to annotate their button definitions. It's now part of the public API. -
5ae18cb: Export the
valueDataTypesandcollectionDataTypesconstants (the scalar and collection subsets of the already-exportedstandardDataTypes). Useful for restrictingallowTypeSelectionto primitives-only or collections-only. -
03f6060: Collection counts now reflect the active search filter, displayed as "n of m" (e.g.
"3 of 20 items") whenever search is narrowing the visible children.showCollectionCountgains a new'when-collapsed-or-filtered'literal that surfaces the count on a collection whenever it's collapsed or a search filter is active — this is the new default, so the n-of-m form is visible without users having to collapse the node. Pass'when-collapsed'for the previous behaviour, or overridecustomText.ITEMS_FILTERED(e.g. returning${size} items) to suppress the n-of-m form entirely.Internally, the per-node
filterNodecascade is replaced with a single post-order walk at the editor level (computeFilterState) that produces both whole-tree visibility and per-collection visible-child counts in one pass. The new walk is also surfaced to nodes via a newFilterStateProvidercontext slice —searchText/searchFilterare no longer threaded as per-node props, which strengthens the §16 node memoization.Two related bug fixes fall out of the rewrite:
- An intermediate collection whose key matched the search filter but whose body was empty (or whose descendants weren't path-aware-matched) used to drop out and drag its ancestors with it. The new walk tests every node, including intermediate collections, so the matching node and its ancestors stay visible. This was observable with a custom
searchFilterthat only inspectedkey, and with the built-in'key'filter on empty{}/[]bodies. indexon the synthesizedchildNodeDataa customsearchFilterreceives is now the position within visible children (matchingbuildNodeDatasemantics); previously it was inherited from the parent and frequently stale.sizeis now the child's actual collection size; previously it was the path depth.
The
ITEMS_FILTEREDlocalisation key (default'{{visible}} of {{total}} items') drives the new display. AcustomText.ITEMS_FILTEREDoverride receives the standardNodeDataplus a newvisibleSizefield carrying the visible-child count alongside the existingsize(the total).The internal
filterNodeandfilterCollectionhelpers are removed — they were never re-exported from the package entry point and aren't user-facing.matchNodeandmatchNodeKeyare unchanged. - An intermediate collection whose key matched the search filter but whose body was empty (or whose descendants weren't path-aware-matched) used to drop out and drag its ancestors with it. The new walk tests every node, including intermediate collections, so the matching node and its ancestors stay visible. This was observable with a custom
-
7cb6ba7: Add a
fromStandardTypefield toCustomNodeDefinition— the inverse oftoStandardType, converting a standard-typed value into the custom type's value. It runs on every confirm of a custom edit (the ✓ button, Enter, Tab,editorRef.confirm()— all paths run this single transform, so ✓ no longer commits the raw edit buffer), where throwing rejects the confirm: nothing commits, the edit session stays open with the user's input intact, and the thrown message surfaces viaonErrorand the inline error display (the same contract as confirming invalid JSON on a collection edit). The same hook seeds the editor when aneditOnTypeSwitchswitch opens the type for editing — the node's current value carries into the switch instead of being replaced bydefaultValue(a throw there seeds the value's string form for the user to fix). Value nodes now show their inline error while editing,editorRef.confirm()no longer tears down a session whose confirm was rejected, and custom components'setValueaccepts anyJsonDatasorenderCollectionAsValuecomponents can buffer object values. -
4b3576c: Add a standalone stylesheet export for Shadow DOM / manual style injection.
The base stylesheet is still inlined and injected into the document
<head>automatically, so the zero-config case is unchanged. It is now also published as a standalone file, importable via thejson-edit-react/style.csssubpath export, for consumers who need to inject the styles themselves — most notably inside a Shadow DOM, where styles injected into the document<head>can't cross the shadow boundary. The stylesheet's custom properties are now defined on both:rootand:hostso they resolve correctly in either context. Resolves #225. -
7cb6ba7: Add a
toStandardTypefield toCustomNodeDefinition: an optional function that converts a custom node's value to a primitive seed when the Type selector switches the node to a standard type. Core's generic coercion handles the rest per target type, and applies unchanged when the definition provides no hook (so e.g. an object-valued custom node without one still seeds'[object Object]'on a switch tostring).
Patch Changes
-
c846bc0: The closing bracket of an expanded object/array now aligns horizontally with the key (the start of the opening line), matching how JSON pretty-printers position a close bracket under the line that opened it. Previously it carried a depth-dependent offset that drifted toward the collapse chevron at the default indent. The alignment is now independent of the
indentprop (#220).Breaking only for custom CSS that positioned the outside closing bracket via
.jer-bracket-outside: it no longer setspadding-left, so any rule that compensated for the old offset should be removed. -
556b1cf: Stabilise the
onCollapsecallback internally so an inline consumer callback no longer churns the collapse context.CollapseProvidernow keepsonCollapsein a ref (mirroring howEditingProvideralready handlesonEditEvent) instead of listing it insetCollapseState's dependencies. Previously, passing a freshonCollapseidentity each render recreatedsetCollapseStateand the collapse context value, re-rendering every node that subscribes to it (everyCollectionNode) on each parent render. Consumers no longer need to memoiseonCollapseto avoid that. No API or behaviour change — the callback still fires once per command with the originalCollapseState. -
99ed120: Fix collapse broadcasts not cascading past the initial mount frontier (#273).
With
collapse={N}and a tree deeper than N, firing a subtree-expand broadcast (e.g. via Opt-click "Open All" orexternalTriggers.collapse) now reaches every descendant — including levels that hadn't yet mounted at broadcast time. Previously the cascade halted at the original mount frontier.Internally,
CollapseProvideris now state-based with a version counter (replacing the pub-sub broadcast introduced in §4 Part 4).handleAddandhandleChangeDataTypeclear the pending broadcast so user-driven new mounts use their default state rather than inheriting a sweeping Collapse-All. No public API change. -
a0872b5: Internal cleanup: remove the now-unused
previouslyEditedElement/recordPreviousEditandtabDirection/setTabDirectionplumbing from the editing store. These were only ever consumed by the redirectuseLayoutEffectretired in the previous Tab-viability work; with the redirect gone, the state was write-only and the actions had no readers.EditingStoreshrinks to{ open, cancel, submit, areChildrenBeingEdited }(plus the subscribe/getSnapshot pair). No user-visible change. -
2cfdeae: Narrowed the
inputHighlighttheme style to a plain colour string. It maps to the editor's text-selection::selectionhighlight, surfaced as a single CSS custom property, so only a static colour is ever meaningful — a style object had only itsbackgroundColorread, and a style function or array did nothing at all. If you previously passed{ backgroundColor: '…' }, pass the colour string directly, e.g.inputHighlight: '#b3d8ff'. -
a20da5f: Bump the rollup TypeScript
targetfromes6toes2020, shrinking the published bundle by ~0.9 kB gzipped (−4.6%).The
react >=18peer dependency already rules out the legacy browsers thates6was downleveling for, so emitting nativeasync/await, object spread, and rest destructuring drops the tslib downlevel helpers (__awaiter,__rest,__spreadArray, …) entirely — they were ~13% of the pre-minified bundle. No source or behaviour change. -
2cfdeae: The two theme colours that can't be applied inline — the input-selection highlight and the copy-button glow — are now rendered as CSS custom properties on the editor container instead of being written to the document root. This scopes them per editor instance (separate themes no longer clobber one another's
--jer-highlight-color/--jer-icon-copy-color), makes them reachable inside a shadow root, and applies them during SSR with no post-hydration flash. The:root, :hostdefaults in the bundled stylesheet still cover any un-themed case — including a new--jer-icon-copy-colordefault, so the copy-button pulse keeps its glow even wheniconCopyis a dynamic style function (which colours the icon per-node but can't collapse to a single container-level value for the pulse). -
14c4eda: Standardize publish workflows across all three packages (tooling-only).
- Core now publishes from a self-contained
build_package/staging directory (set viapublishConfig.directory). Replaces the fragileprepublishOnlyswap-and-restore dance; a failed publish can no longer leave the repo with a half-swapped README. - Short-README link rewriting in
scripts/build_npm_readme.pynow handles relative file links (e.g.[migration-guide.md](migration-guide.md)) in addition to anchor links, so npm-page links render correctly. - Sub-packages gain a
prepack: pnpm buildguard sopnpm pack/pnpm publishalways ship a fresh build, and apreview-publishscript that produces an inspectable.tgz. - Sub-package builds now clean up
build/dts/intermediate output, so the published tarballs no longer include those redundant declaration files.
Published runtime behaviour is unchanged.
- Core now publishes from a self-contained
-
a0872b5: Tab navigation now skips filtered-out and non-editable nodes up front instead of opening them and bouncing reactively. The redirect
useLayoutEffectinValueNodeWrapperthat previously fired transientstartEdit/cancelEditobserver events on dead-end nodes is gone —onEditEventconsumers no longer see those spurious pairs.The change is in
getNextOrPrevious, which gains an optional 5thisViable?: (nodeData) => booleanpredicate.useCommoncomposes the predicate from the precomputedfilterState.visiblePathsSet and the existingallowEditFilter, and threads it through. Tab navigation from both value edits and key edits (viaKeyDisplay) now benefits.Behaviour change worth noting: when no viable Tab target exists, the editor cancels cleanly. The previous "fall back to
previouslyEditedElement" hop is gone.When live search hides the actively-edited node, the input now unmounts cleanly and the editing record sits inactive in the store; clearing the search later resumes the edit. No off-screen commit footgun because there's no input element to commit through.
-
de1cd5d: Harden the text-editing fields against host-app CSS. The string and raw-JSON editors now pin
box-sizingand an explicitline-heightinstead of leaving them to inherit, so a consuming app's global reset (e.g.* { box-sizing: border-box }) or an element-leveltextarea/inputrule can no longer distort their layout or text wrapping. This also keeps the auto-growing textarea's hidden measuring element locked to the real<textarea>, fixing a latent case where the raw-JSON editor could mis-measure its height even with no host reset present.The pinned
box-sizingiscontent-box(the model the editor was designed against), so it's a no-op for consumers without a global reset. The raw-JSON editor's line spacing is now set explicitly and may shift very slightly. -
ece6d70: Fix
CustomNodeDefinition'sUgeneric so it bindswrapperProps, not justwrapperComponent. The generic exists to keep awrapperComponentand thewrapperPropsconfig object it receives in sync, butwrapperPropswas typedRecord<string, unknown>, so a mismatch between the two went uncaught at compile time. It's nowwrapperProps?: U, mirroring howcomponentProps?: Talready binds the value-component generic. Default (un-parameterized) usage is unchanged, sinceUdefaults toRecord<string, unknown>.