Skip to content

Configurable atomic vs. recursive treatment of arrays and objects in dirty methods #114

@fabian-hiller

Description

@fabian-hiller

Summary

Add configuration options to the new dirty methods (getDirtyPaths, getDirtyInput, pickDirty) that control whether arrays and objects are treated as atomic or are recursed into when collecting dirty state.

This builds on the dirty methods introduced in #98.

Current behavior

The dirty methods currently apply a fixed, asymmetric strategy:

  • Arrays are atomic — if any item is dirty (or the length changed), only the array's own path is reported, e.g. ['items'] rather than ['items', 1, 'name'].
  • Objects are recursed into — dirty descendants contribute their full paths, e.g. ['user', 'email'].

So given:

{ users: [{ name: 'John' }, { name: 'Jane' }] }
// mark users[0].name dirty -> 'Johnny'
getDirtyPaths(form) // => [['users']]

vs.

{ user: { email: 'a@example.com', name: 'John' } }
// mark user.email dirty -> 'b@example.com'
getDirtyPaths(form) // => [['user', 'email']]

Motivation

The atomic-vs-recursive choice is hardcoded today, but different use cases want different granularity:

  • Atomic is ideal when you submit a whole array/object as one unit (e.g. send the full users array to the backend on any change).
  • Recursive is ideal when you want fine-grained diffs (e.g. PATCH only the exact nested fields that changed).

Making this configurable lets consumers pick the granularity that matches their submit/diff strategy instead of working around the fixed behavior.

Proposed API

Add two independent boolean options to the dirty methods config:

getDirtyPaths(form, { atomicArrays: true, atomicObjects: false });
Option Default Effect
atomicArrays true Report only the array's own path when any item is dirty (current behavior).
atomicObjects false Report only the object's own path when any property is dirty.

The defaults preserve today's behavior (arrays atomic, objects recursive). Both options are threaded through getDirtyInput and pickDirty so all three methods stay consistent.

Why two booleans instead of a single atomic enum

An enum like 'none' | 'arrays' | 'all' was considered, but two independent booleans are a better fit:

  • They express every combination. The full matrix is 2×2; an enum that ranks arrays as "more atomic" than objects can't represent recursive-arrays-but-atomic-objects:

    atomicArrays atomicObjects enum equivalent
    true false 'arrays' (current default)
    true true 'all'
    false false 'none'
    false true ❌ not expressible
  • Consistent with existing conventions. Add dirtyOnly option to getFieldInput and getInput for filtering dirty fields #96 already adds a dirtyOnly boolean in this same dirty-state area, so atomicArrays / atomicObjects read as natural siblings rather than a new pattern.

  • Self-documenting. { atomicArrays: true } needs no lookup table; an enum string ("what does 'arrays' cover again?") does.

  • Cleaner at the type level. Each flag is an independent literal boolean that drives its own conditional in DirtyPath, instead of pattern-matching one string union across the array and object branches.

The only thing the enum buys us is blocking the niche recursive-arrays/atomic-objects combo and being terser for the common modes — not worth losing the expressiveness and convention-fit.

Note on the root

The root object should be excluded from atomic treatment regardless of atomicObjects — collapsing the entire form into a single top-level dirty path would defeat the purpose of these methods. Atomic treatment applies only to nested arrays/objects.

Open questions

  • Confirm the defaults (atomicArrays: true, atomicObjects: false) to stay backwards compatible.
  • Type-level implications for DirtyPath — the returned path union currently encodes array atomicity at the type level (arrays fall through to their own path). Making this configurable likely requires both flags to influence the inferred return type.

References

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions