Skip to content

Filter-function toolkit for /utils — composable predicates for allow* props and searchFilter #343

@CarlosNZ

Description

@CarlosNZ

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    V2To include in Version 2utilsPossible "utility" helpers that can live outside the package (maybe in its own package)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions