Another candidate for the @json-edit-react/utils package: a small kit of composable predicate builders for the allow* props and searchFilter. Hand-writing FilterFunctions over NodeData is fiddly (especially path matching) — allowEdit={byKey('name', 'email')} reads far better. Zero-dep, pure logic.
The organizing idea: builders produce predicates over NodeData, and because a FilterFunction ((input: NodeData) => boolean) is assignable to a SearchFilterFunction ((input: NodeData, searchText: string) => boolean) — extra args just pass through — one set of combinators serves the allow* props and searchFilter with no search-specific variants.
Builders
| Builder |
Matches when… |
Example |
byKey(...keys) |
node's own key is one of these (string or RegExp) |
allowEdit={byKey('name', 'email')} |
byPath(pattern) |
node's path matches a glob string, RegExp, or segment array |
allowDelete={not(byPath('config.**'))} |
byLevel({ min?, max? }) |
depth within range |
allowAdd={byLevel({ max: 2 })} |
byType(...types) |
value type incl. 'object' / 'array' |
allowTypeSelection={byType('string', 'number')} |
byValue(pred) |
escape hatch on the value |
allowEdit={not(byValue((v) => v === 'LOCKED'))} |
bySize({ min?, max? }) |
collection child-count within range |
allowAdd={bySize({ max: 9 })} (cap list length) |
root |
the root node (constant, = byLevel({ max: 0 })) |
allowDelete={not(root)} |
collections / primitives |
objects + arrays / leaf values |
allowDrag={primitives} |
inArray / inObject |
parent is an array / object |
allowDrag={inArray} (reorder list items only) |
Combinators
and(...fns), or(...fns), not(fn) — variadic, signature-generic (work for both FilterFunction and SearchFilterFunction).
Putting it together
// Whitelist: only contact fields are editable, everything else read-only
<JsonEditor allowEdit={byPath('users.*.{name,email}')} ... />
// Lock a subtree and the root; everything else stays editable
<JsonEditor allowEdit={not(or(byPath('metadata.**'), root))} ... />
// Adds only near the top, deletes only on array items, drag within arrays
<JsonEditor
allowAdd={byLevel({ max: 1 })}
allowDelete={inArray}
allowDrag={inArray}
/>
// Search only within the users subtree, matching on key or value
<JsonEditor searchFilter={and(byPath('users.**'), matchesSearch('all'))} ... />
The last example shows the cross-compose payoff: byPath ignores the searchText arg, so the same builder scopes a search without a search-specific variant. matchesSearch('key' | 'value' | 'all') is the thin bridge wrapping core's exported matchNode / matchNodeKey — which is also the natural seam with #319 (the ready-made search filters are pre-composed instances of this kit). The kit also sits alongside the schema-derived filters of #285 as the general hand-rolled option.
Design notes
- Path patterns:
* = one segment, ** = any run of segments, {a,b} = alternation, numeric segments for array indices. The segment-array form (byPath(['users', '*', 'email'])) is the unambiguous escape hatch for keys that contain . — same reason core uses CollectionKey[] paths internally. Compile the glob to a segment matcher once at builder-call time, not per invocation — these predicates run per node per render.
- Subtree vs exact:
byPath('metadata') is exact; byPath('metadata.**') is the subtree. Hand-rolled path checks almost always get the descendant case wrong — that's half the value of the kit.
- Polarity reads naturally with the
allow* props: allowEdit={byKey(...)} is a whitelist, allowEdit={not(byKey(...))} a blacklist — no double-negative gymnastics.
Another candidate for the
@json-edit-react/utilspackage: a small kit of composable predicate builders for theallow*props andsearchFilter. Hand-writingFilterFunctions overNodeDatais fiddly (especially path matching) —allowEdit={byKey('name', 'email')}reads far better. Zero-dep, pure logic.The organizing idea: builders produce predicates over
NodeData, and because aFilterFunction((input: NodeData) => boolean) is assignable to aSearchFilterFunction((input: NodeData, searchText: string) => boolean) — extra args just pass through — one set of combinators serves theallow*props andsearchFilterwith no search-specific variants.Builders
byKey(...keys)allowEdit={byKey('name', 'email')}byPath(pattern)allowDelete={not(byPath('config.**'))}byLevel({ min?, max? })allowAdd={byLevel({ max: 2 })}byType(...types)'object'/'array'allowTypeSelection={byType('string', 'number')}byValue(pred)allowEdit={not(byValue((v) => v === 'LOCKED'))}bySize({ min?, max? })allowAdd={bySize({ max: 9 })}(cap list length)rootbyLevel({ max: 0 }))allowDelete={not(root)}collections/primitivesallowDrag={primitives}inArray/inObjectallowDrag={inArray}(reorder list items only)Combinators
and(...fns),or(...fns),not(fn)— variadic, signature-generic (work for bothFilterFunctionandSearchFilterFunction).Putting it together
The last example shows the cross-compose payoff:
byPathignores thesearchTextarg, so the same builder scopes a search without a search-specific variant.matchesSearch('key' | 'value' | 'all')is the thin bridge wrapping core's exportedmatchNode/matchNodeKey— which is also the natural seam with #319 (the ready-made search filters are pre-composed instances of this kit). The kit also sits alongside the schema-derived filters of #285 as the general hand-rolled option.Design notes
*= one segment,**= any run of segments,{a,b}= alternation, numeric segments for array indices. The segment-array form (byPath(['users', '*', 'email'])) is the unambiguous escape hatch for keys that contain.— same reason core usesCollectionKey[]paths internally. Compile the glob to a segment matcher once at builder-call time, not per invocation — these predicates run per node per render.byPath('metadata')is exact;byPath('metadata.**')is the subtree. Hand-rolled path checks almost always get the descendant case wrong — that's half the value of the kit.allow*props:allowEdit={byKey(...)}is a whitelist,allowEdit={not(byKey(...))}a blacklist — no double-negative gymnastics.