Skip to content

feat!: array-element field identity via @solidjs/signals store - #6

Open
LouisHaftmann wants to merge 1 commit into
masterfrom
feat/solidjs-signals-store
Open

feat!: array-element field identity via @solidjs/signals store#6
LouisHaftmann wants to merge 1 commit into
masterfrom
feat/solidjs-signals-store

Conversation

@LouisHaftmann

Copy link
Copy Markdown
Contributor

Closes #4.

What & why

Replaces @vue/reactivity and the hand-written on-change array-mutation mirror with the @solidjs/signals store (SolidJS 2.0, next) as form-core's reactive engine. Array-element identity is now maintained by the engine, so a FormField travels with its datum: reorder / splice / sort / reverse / unshift / move keep each element's key, dirty state, and errors attached to the element, not the index. This removes the fragile per-method mirror the issue set out to kill.

Key changes

  • Field cache re-keyed by store-node identity (WeakMap<node, { self, children }>). Primitive-leaf array elements stay positional (the accepted hybrid). Deleted the nested _array cache, getFieldCachePath, the on-change onValidate/onChange mirror, and the data-path → cache-path transform.
  • Writable form.data facade — a thin path-tracking proxy over the store; reads pass through (reactive, identity-stable), writes route into the engine setter. New form.setData(recipe) applies a batch atomically with a single validation.
  • Asynchronous flush contract — writes are microtask-batched; value-level derived state is current only after the next flush. No public settled promise; a hidden form['~'].flush() escape hatch forces the pending batch current (used by imperative/test code and the framework bindings).
  • Whole-form validation driven by a single deep(store) effect (replacing the on-change global callback); per-field issue filtering by path is unchanged; on-change/on-blur/on-submit timing preserved.
  • reset() / non-dirty sourceValues updates use reconcile — positional by default (reproduces prior reset behavior, keeps fields stable), with an optional per-form reconcileKey resolver to preserve identity across an id-keyed external refresh. Dirty-guard unchanged.
  • Adapters rewritten. React bridges the store into re-renders via useSyncExternalStore; Vue bridges store reactivity into Vue and keeps field.model as a writable v-model computed. Migrated the React hook tests off the React-19-incompatible @testing-library/react-hooks.
  • Data purity preserved (snapshot() feeds validation/submit); field.schema, translate, discriminator, hooks unchanged.

Implementation note: identity + the beta store

The beta store clones an element that still carries a pending write-override when its array is reordered, which would sever the field's identity after an edit. To honor the headline user story (drag-reorder of edited rows keeps focus/dirty), each array-restructuring op first bakes the store (reconcile(snapshot(store))) to collapse the override layer — transparent to values and node identity. It's an O(n) step at form scale, pinned to @solidjs/signals@2.0.0-beta.24; the pre-release dependency and its blast radius were accepted in the issue.

Breaking changes (pre-1.0)

  • form-core now depends on @solidjs/signals instead of @vue/reactivity; the @falcondev-oss/form-core/reactive entrypoint was removed.
  • Read/write contract is asynchronous — imperative code must await a tick or call form['~'].flush().
  • field.key's string format changed; array.delete(key) resolves the element by identity instead of parsing an index.

Tests

Core (49), React (2), Vue (2) all green; tsc --build, ESLint/Prettier, and pnpm build clean. Identity is asserted through the public handle only (same key, preserved dirty/errors, correct at(i) after a move), including the edit-then-reorder case; primitive-array positional behavior, delete(key) after reorder, setData single-validation, reset, dirty-guard, and the key resolver are all covered.

🤖 Generated with Claude Code

Replace the @vue/reactivity engine and the hand-written on-change array
mutation mirror with the @solidjs/signals store as form-core's reactive
engine. Array-element identity is now maintained by the engine: a field
rides along with its datum across splice/sort/reverse/unshift/move, so its
key, dirty state, and errors follow the element instead of the index.

- Field cache re-keyed by store-node identity (WeakMap<node>); primitive
  leaves remain positional (accepted hybrid). Removes the nested `_array`
  cache, getFieldCachePath, and the on-change onValidate/onChange mirror.
- `form.data` becomes a writable path-tracking facade over the store;
  `form.setData(recipe)` applies a batch atomically with one validation.
- Write contract is now asynchronous (microtask-batched); a hidden
  `form['~'].flush()` escape hatch forces the pending batch current.
- Whole-form validation is driven by a single `deep(store)` effect;
  per-field issue filtering by path is unchanged.
- reset()/non-dirty sourceValues updates use `reconcile` (positional by
  default) with an optional per-form `reconcileKey` resolver.
- React adapter re-renders via a Solid effect bridge; Vue adapter bridges
  store reactivity to Vue and keeps `field.model` as a writable computed.

BREAKING CHANGE: form-core now depends on @solidjs/signals instead of
@vue/reactivity; the write/read contract is asynchronous (use
`form['~'].flush()` in imperative code); `field.key`'s format changed and
`array.delete(key)` resolves by identity rather than parsing an index; the
`@falcondev-oss/form-core/reactive` entrypoint was removed.

Closes #4

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Array-element field identity via @solidjs/signals store (replace on-change sync)

1 participant