Skip to content

new: form package for Solid 2.0#926

Open
davedbase wants to merge 17 commits into
solidjs-community:nextfrom
davedbase:v2/form
Open

new: form package for Solid 2.0#926
davedbase wants to merge 17 commits into
solidjs-community:nextfrom
davedbase:v2/form

Conversation

@davedbase

@davedbase davedbase commented Jun 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Introduces createForm<C> and toFormData for Solid 2.0
  • Per-field signals (value, error, touched, pending), form-level signals (dirty, valid, submitting, submitted), and DOM binding via the 2.0 two-phase ref directive pattern
  • Sync and async validators are first-class; validators are plain (value) => string | null | Promise<string | null> functions — no adapter needed for Zod, Valibot, Arktype, etc.
  • validateOn: "change" | "blur" | "submit" controls error display timing, per field or form-wide
  • Cross-field rules via form.validate(fn); server-side errors via form.setError(name, msg)
  • SSR-safe: static accessors on the server, no reactive primitives created

Implementation notes (fixes applied during review)

Bug fix — async validators fired 3× per keystroke
The original architecture called each validator from three code paths per value change: once from the _rawError memo, once from the effect's sync-check, and once from the effect's collection loop. For async validators making network requests, this meant 3 API calls per keystroke with only the last result used.

Fixed by pre-classifying validators at setup time (one probe call each with fc.initial): sync validators go into a lazy syncMemo that is the only reactive subscriber to value() and never touches async validators; async validators go into asyncFns. The initial async Promises from classification are saved and reused on the first effect run if the value hasn't changed, avoiding a second API call. When async settles and asyncError() changes, _rawError recomputes by reading signals only — no validator is called. Result: async validators fire once per value change.

toFormDatafalse now omitted
Previously false was coerced to the string "false", which doesn't match HTML's native behavior (unchecked checkboxes are absent from form payloads). false is now omitted alongside null and undefined.

reset(newValues?) overload
Accepts an optional partial values object. Named fields adopt the new values as their dirty baseline, making edit-form patterns possible without fighting the initial-value comparison. A version counter forces dirty to recompute even when the field value and new baseline are identical (no-op signal write wouldn't invalidate the memo otherwise).

setError / field.setError
Injects an external (server-side) error into the reactive graph. Appears in field.error(), form.errors(), and gates form.valid(). Cleared automatically when the user edits the field (setValue calls the wrapped setter which clears it).

Documentation fixes

  • README type definition for bind was missing | HTMLTextAreaElement
  • validate() note incorrectly said "before form.valid() is first read" — the version counter handles late registration
  • errors() table row now documents the exclusion of cross-field validate() errors

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced createForm primitive for building reactive forms with integrated validation.
    • Configurable validation timing: on change, blur, or submit.
    • Automatic form element binding and two-way DOM synchronization.
    • Cross-field validation rules for complex scenarios.
    • Async validator support and FormData export for server submission.

@davedbase davedbase added this to the Solid 2.0 Migration milestone Jun 3, 2026
@changeset-bot

changeset-bot Bot commented Jun 3, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 9122fca

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 142978cc-33b1-47c6-abbb-258e4125e7f3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@davedbase davedbase marked this pull request as ready for review June 3, 2026 12:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/form/README.md`:
- Around line 100-115: The README's public API table and method list omit the
public method setValues, which is declared on FormReturn and implemented in
form.ts; add an entry for setValues to the API table and the definition block
describing its signature and behavior (e.g., setValues(newValues?:
Partial<Values>) => void, updates current values and optionally resets
baselines), referencing the same terminology as FormReturn and the
implementation in form.ts so documentation and code remain consistent.

In `@packages/form/src/form.ts`:
- Line 100: The SSR branch currently returns a blank FormData via formData: ()
=> new FormData(), causing mismatch with client behavior which uses
toFormData(values()); change the SSR stub so formData() returns the serialized
initial values by calling toFormData(values()) (ensure toFormData is
imported/available and values() is referenced the same way as in the client
branch) so server consumers receive the same payload as the hydrated client.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e75dd6f3-96a8-4c3e-bca6-67e50d15352b

📥 Commits

Reviewing files that changed from the base of the PR and between fd4b2c2 and 9122fca.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • packages/form/README.md
  • packages/form/package.json
  • packages/form/src/form.ts
  • packages/form/src/index.ts
  • packages/form/src/types.ts
  • packages/form/stories/form.stories.tsx
  • packages/form/test/index.test.ts
  • packages/form/test/server.test.ts
  • packages/form/tsconfig.json

Comment thread packages/form/README.md
Comment on lines +100 to +115
| Member | Type | Description |
|--------|------|-------------|
| `dirty()` | `Accessor<boolean>` | `true` when any field differs from its initial value |
| `valid()` | `Accessor<boolean>` | `true` when all fields pass validation and no async validators are pending |
| `pending()` | `Accessor<boolean>` | `true` while any field has an async validator in flight |
| `submitting()` | `Accessor<boolean>` | `true` while `onSubmit` is in flight |
| `submitted()` | `Accessor<boolean>` | `true` after the first submit attempt; reset by `reset()` |
| `values()` | `Accessor<Values>` | Plain object of all current field values |
| `errors()` | `Accessor<Partial<Record<...>>>` | Object containing only **field-level** errors (always reflects true validity, regardless of `validateOn`). Cross-field errors registered via `validate()` are not included — access those through the accessor each `validate()` call returns. |
| `bind(name)` | Ref directive factory | Wires an `<input>` or `<select>` to the named field (see below) |
| `ref` | Ref callback | Attaches submit handling to a `<form>` element |
| `validate(fn)` | Cross-field rule | Registers a form-level validation rule (see below) |
| `setError(name, msg)` | `(name, error: string \| null) => void` | Inject an external error on a named field (e.g. server-side validation). Cleared automatically when that field's value changes. |
| `formData()` | `() => FormData` | Snapshot of current field values as a `FormData` instance |
| `reset(newValues?)` | `(newValues?: Partial<Values>) => void` | Reset all fields. If `newValues` is provided, the named fields adopt those as their new baseline (useful for edit forms after a successful save). |
| `submit()` | `() => Promise<void>` | Programmatically trigger submission |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document setValues in the public API section.

setValues is part of FormReturn in packages/form/src/types.ts and is implemented in packages/form/src/form.ts, but it is missing from both the form-level API table and the definition block here. That leaves a shipped public method undocumented.

Also applies to: 413-429

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/form/README.md` around lines 100 - 115, The README's public API
table and method list omit the public method setValues, which is declared on
FormReturn and implemented in form.ts; add an entry for setValues to the API
table and the definition block describing its signature and behavior (e.g.,
setValues(newValues?: Partial<Values>) => void, updates current values and
optionally resets baselines), referencing the same terminology as FormReturn and
the implementation in form.ts so documentation and code remain consistent.

Comment thread packages/form/src/form.ts
validate: () => () => null,
setValues: () => void 0,
setError: () => void 0,
formData: () => new FormData(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep formData() consistent in the SSR branch.

The server stub returns new FormData() even though values() still exposes the configured initial values and the client branch serializes those values through toFormData(values()). Any SSR consumer calling form.formData() gets a different payload than the hydrated client.

Suggested fix
-      formData: () => new FormData(),
+      formData: () => toFormData(initialValues),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
formData: () => new FormData(),
formData: () => toFormData(initialValues),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/form/src/form.ts` at line 100, The SSR branch currently returns a
blank FormData via formData: () => new FormData(), causing mismatch with client
behavior which uses toFormData(values()); change the SSR stub so formData()
returns the serialized initial values by calling toFormData(values()) (ensure
toFormData is imported/available and values() is referenced the same way as in
the client branch) so server consumers receive the same payload as the hydrated
client.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant