Skip to content

Tanstack form basics#64

Open
publicJorn wants to merge 31 commits into
mainfrom
tanstack-form-basics
Open

Tanstack form basics#64
publicJorn wants to merge 31 commits into
mainfrom
tanstack-form-basics

Conversation

@publicJorn

@publicJorn publicJorn commented Jun 17, 2026

Copy link
Copy Markdown
Member

Closes #41.

Process our findings of the studio day. I make the commits so that they are feature-based and the files in it not (heavily) updated one after another. So that may be a good way to review this bulky change.

  • TODO: update existing forms (eeh.. login page). Just wanted to run this by the reviewers already
  • Update example api spec (and error utils) so they represent what we discussed with BE

Here's an AI summary:


Introduces a reusable TanStack Form (TSF) setup includes a working form-example route and adoption docs.

What's new

Form composition (lib/forms/, components/form/)

  • useAppForm / withForm hook wiring field connectors in one place.
  • Connectors prefixed tsf- wrap base-ui primitives and bind them to TSF
  • Shared Field wrapper handles label / description / error / required, plus form.scss mixins (invalid, disabled) reused across controls.
  • Non-connected variants (e.g. checkbox.tsx) kept separate, per the tsf- convention.

Validation helpers (lib/forms/validation-helpers.ts) ⭐

These are the key utilities that make TSF easier to work with:

  • mutateAndValidate(mutation) — bridges TanStack Query mutations into TSF's onSubmitAsync, turning backend errors into form/field errors. Temporary shim until TSF#2188.
  • apiErrorToFormErrors(error) — maps an RFC9457 problem-details API error onto { form, fields }, converting snake_case paths to camelCase.
  • normalizeFieldErrors(errors) — flattens TSF's mixed error shapes (string / {message} / arrays) into string[].
  • getFieldErrors(state, fields) — pulls normalized errors for a set of fields.
  • Backed by lib/api/error-helpers.ts (parseErrorString) for unknown/poorly-typed errors.

Docs & example

A new docs folder that should help people adopt the template. The idea of the docs/snippets/ folder is to have practical examples of preferred implementations (but should not be in code -lest it needs to be removed).

@publicJorn publicJorn requested a review from maanlamp as a code owner June 17, 2026 06:47
@publicJorn publicJorn marked this pull request as draft June 17, 2026 07:53
@publicJorn publicJorn requested a review from Atchferox June 30, 2026 14:30
@publicJorn publicJorn changed the base branch from main to feature/localised-page-titles June 30, 2026 14:33
@Atchferox

Copy link
Copy Markdown
Contributor

I wil have a look at this tomorrow!

@Atchferox

Copy link
Copy Markdown
Contributor

Really into this direction overall. One structural thing I'd like us to lock in now, while the pattern is still greenfield and there's nothing to migrate:

1. Drop the tsf- prefix — name these by what they are: fields.

tsf- bakes the implementation library (TanStack Form) into the component name. That's an implementation detail — same reason we wouldn't call something redux-counter or zustand-cart. If we ever swap form libs the name lies, and to a reader tsf-checkbox doesn't describe what the thing is.

What these components actually are is form fields: a control + label + description + error + required, composed together. We already have a Field component doing exactly that composition, and base-ui calls it Field too. So I'd propose:

  • tsf-checkbox.tsxcheckbox-field.tsx (CheckboxField)
  • tsf-input.tsxinput-field.tsx (InputField)
  • tsf-checkbox-group.tsxcheckbox-group-field.tsx (CheckboxGroupField)

Reads naturally: "a Field wrapping a Checkbox." The registry keys in lib/forms/index.tsx can stay as-is (field.Checkbox still works) — only the file/component names change.

2. Keep the primitive a pure primitive.

Right now Checkbox (checkbox.tsx) does two jobs: it renders the base-ui control and its own <label>. I'd keep Checkbox as just the styled base-ui primitive (Root + Indicator), and let CheckboxField own label/description/error — same as InputField/Field already do. Upsides:

  • The primitive stays reusable outside forms (a checkbox in a toolbar/filter that isn't a TSF field).
  • One consistent answer to "where does the label live" — always the field layer.

Caveat worth noting: a checkbox label sits inline next to the box and toggles it, unlike an input label which sits above. So CheckboxField would place the label in the checkbox-appropriate spot rather than reusing Field.Label verbatim — but that's exactly the field layer's job.

3. This also cleans up an inconsistency that's already in the diff.

  • tsf-input imports base-ui Input directly and skips the local Input primitive, while tsf-checkbox composes the Checkbox primitive.
  • Label placement diverges: tsf-input passes label to Field, tsf-checkbox passes it inline to the primitive.

Standardising on "primitive = pure base-ui wrapper, XField = primitive + Field" makes every field wrapper look identical and removes the guesswork about what goes where.

One follow-up: docs/working-with-forms.md currently documents the tsf- convention, so we'd update that too.

Happy to pair on the rename since it's all new code anyway.

Comment thread docs/recipes/extending-base-ui.md
Comment thread src/components/form/field/field.tsx Outdated
Comment thread src/components/error-text/error-text.tsx

@maanlamp maanlamp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good overall.

  • While I do think the tsf prefix is not great, I understand its purpose as it really is locked into the tanstack form hooks. If we want to make that agnostic, we'd have to abstract those hooks away which I don't think is worth the effort. Let's hope we don't do any major lib migrations anytime soon 😉
  • I think its important to be consistent with the field/input naming. Checkbox would need a field as well as a "raw" checkbox. That aligns with the rest of the form controls. This also fixes the latent issue that we can't compose a checkbox with some other UI without dragging along the label and form semantics in this current impl.

Comment thread docs/recipes/extending-base-ui.md
Comment thread src/components/error-text/error-text.tsx
Base automatically changed from feature/localised-page-titles to main July 7, 2026 12:13
@publicJorn

Copy link
Copy Markdown
Member Author

Made some solid improvements based on the feedback.

BaseUI is now extended as described in the docs for the form elements.
The checkbox variants especially are now done as BaseUI expects it to be handled (thanks Luuk!).

I kept in some console.logs and superfluous error messages, so that consumers can really understand how it works. It it still easy to remove the whole example.

I'm pretty happy with this. Please re-review!

@publicJorn publicJorn marked this pull request as ready for review July 7, 2026 14:01
@publicJorn publicJorn requested review from Atchferox and maanlamp July 7, 2026 14:01
Comment thread src/components/form/field/field.tsx Outdated
Comment thread src/lib/forms/validation-helpers.ts
Comment thread src/lib/forms/validation-helpers.ts
Comment thread src/components/form/input/tsf-input.tsx Outdated
Comment thread docs/recipes/extending-base-ui.md Outdated
Comment thread src/components/header/app-header.tsx Outdated
Comment thread src/components/header/app-header.tsx
Comment thread src/routes/_app/form-example.tsx
@maanlamp

maanlamp commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Looks good overall, I can get behind this! There's a few merge conflicts and I found things I think are important to address before we merge.

@publicJorn

Copy link
Copy Markdown
Member Author

Looks good overall, I can get behind this! There's a few merge conflicts and I found things I think are important to address before we merge.

Merged it this morning and processed comments!

@publicJorn publicJorn requested a review from maanlamp July 8, 2026 07:44
@Atchferox

Copy link
Copy Markdown
Contributor

I think overall we are now in the right direction to use this as a proper template 🚀, one thing left to fix is the build 😆

@publicJorn

Copy link
Copy Markdown
Member Author

Build & nitpick fixed. @maanlamp can you check the comments I wrote and resolve if you are ok with it?
Then let's get this merged!

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.

Base UI implementation

3 participants