feat(components): refactor form to tanstack#2255
Conversation
There was a problem hiding this comment.
Pull request overview
Migrates the app’s form handling away from Vuelidate/Vee-Validate to @tanstack/vue-form + Zod, removing legacy validation wrappers/components and introducing a new field UI composition layer for consistent form layout and error rendering.
Changes:
- Add
@tanstack/vue-formdependency and removevuelidate/vee-validatepackages and related Nuxt transpilation entries. - Refactor multiple form components (auth, support, contact/event forms, preferences) to TanStack Form APIs + Zod schemas.
- Introduce new
scn/field+scn/separatorprimitives and new form utilities (isFieldInvalid,normalizeFieldErrors) while removing legacyAppForm/FormInput*/scn/formhelpers.
Reviewed changes
Copilot reviewed 67 out of 68 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/package.json | Add TanStack Form; remove legacy validators |
| src/nuxt.config.ts | Remove legacy deps from transpile list |
| src/app/utils/validation.ts | Remove Vuelidate exports; keep API-based validators |
| src/app/utils/form.ts | Add TanStack field meta helpers |
| src/app/pages/guest/view/[id]/index.vue | Replace FormInputStateInfo with plain text |
| src/app/pages/event/view/[username]/[event_name]/attendance.vue | Replace FormInputStateInfo with plain text |
| src/app/pages/account/create/index.vue | Capture registration values via submit payload |
| src/app/composables/formSubmit.ts | Remove vee-validate submit composable |
| src/app/composables/form.ts | Remove Vuelidate-based useAppForm |
| src/app/components/scn/separator/Separator.vue | New separator primitive (reka-ui wrapper) |
| src/app/components/scn/separator/index.ts | Export separator primitive |
| src/app/components/scn/label/Label.vue | Delegate props via reactiveOmit |
| src/app/components/scn/form/useFormField.ts | Remove vee-validate field context helper |
| src/app/components/scn/form/injectionKeys.ts | Remove legacy injection key |
| src/app/components/scn/form/index.ts | Remove vee-validate form exports |
| src/app/components/scn/form/FormMessage.vue | Remove vee-validate error message wrapper |
| src/app/components/scn/form/FormLabel.vue | Remove legacy label wrapper |
| src/app/components/scn/form/FormItem.vue | Remove legacy form item wrapper |
| src/app/components/scn/form/FormDescription.vue | Remove legacy description wrapper |
| src/app/components/scn/form/FormControl.vue | Remove legacy control wrapper |
| src/app/components/scn/field/index.ts | New field component exports + variants |
| src/app/components/scn/field/FieldTitle.vue | New field title component |
| src/app/components/scn/field/FieldSet.vue | New fieldset layout component |
| src/app/components/scn/field/FieldSeparator.vue | New separator-with-label layout |
| src/app/components/scn/field/FieldLegend.vue | New legend/label variant component |
| src/app/components/scn/field/FieldLabel.vue | New label wrapper for fields |
| src/app/components/scn/field/FieldGroup.vue | New field group/container component |
| src/app/components/scn/field/FieldError.vue | New error list/single message renderer |
| src/app/components/scn/field/FieldDescription.vue | New description component |
| src/app/components/scn/field/FieldContent.vue | New content wrapper component |
| src/app/components/scn/field/Field.vue | New field wrapper using fieldVariants |
| src/app/components/preference/form/PreferenceFormSize.vue | Migrate preference form to TanStack Form |
| src/app/components/guest/GuestList.vue | Replace FormInputStateInfo with plain text |
| src/app/components/form/radio/FormRadioGroupItem.vue | Remove form-specific radio item |
| src/app/components/form/input/state/FormInputStateWarning.vue | Remove legacy Vuelidate warning state |
| src/app/components/form/input/state/FormInputStateSuccess.vue | Remove legacy Vuelidate success state |
| src/app/components/form/input/state/FormInputStateInfo.vue | Remove legacy Vuelidate info state |
| src/app/components/form/input/state/FormInputStateError.vue | Remove legacy Vuelidate error state |
| src/app/components/form/input/state/FormInputState.vue | Remove legacy input state wrapper |
| src/app/components/form/input/FormInputUsername.vue | Remove legacy Vuelidate username input |
| src/app/components/form/input/FormInputUrl.vue | Remove legacy URL input component |
| src/app/components/form/input/FormInputPhoneNumber.vue | Remove legacy phone input component |
| src/app/components/form/input/FormInputPassword.vue | Remove legacy password input component |
| src/app/components/form/input/FormInputEmailAddress.vue | Remove legacy email input component |
| src/app/components/form/input/FormInputCaptcha.vue | Remove legacy captcha input component |
| src/app/components/form/input/FormInput.vue | Remove legacy generic input component |
| src/app/components/form/FormSupportReport.vue | Refactor support report form to TanStack Form |
| src/app/components/form/FormSupportIssue.vue | Refactor support issue form to TanStack Form |
| src/app/components/form/FormSupportIdea.vue | Refactor support idea form to TanStack Form |
| src/app/components/form/FormSupportContact.vue | Refactor support contact form to TanStack Form |
| src/app/components/form/FormSessionCreate.vue | Refactor sign-in form to TanStack Form |
| src/app/components/form/FormGuest.vue | Refactor guest selection form to TanStack Form |
| src/app/components/form/FormEvent.vue | Refactor event create/edit form to TanStack Form |
| src/app/components/form/FormEarlyBird.vue | Refactor early-bird form to TanStack Form |
| src/app/components/form/FormDelete.vue | Refactor delete confirmation form to TanStack Form |
| src/app/components/form/FormContact.vue | Refactor contact create/edit form to TanStack Form |
| src/app/components/form/field/FormFieldConsent.vue | Remove legacy consent field wrapper |
| src/app/components/form/account/registration/FormAccountRegistrationAge.vue | Refactor age step to TanStack Form |
| src/app/components/form/account/registration/FormAccountRegistration.vue | Refactor registration form to TanStack Form |
| src/app/components/form/account/password/FormAccountPasswordResetRequest.vue | Refactor reset request form to TanStack Form |
| src/app/components/form/account/password/FormAccountPasswordReset.vue | Refactor reset form to TanStack Form |
| src/app/components/form/account/password/FormAccountPasswordChange.vue | Refactor password change to TanStack Form |
| src/app/components/form/account/FormAccountLegalConsent.vue | Refactor legal consent to TanStack Form |
| src/app/components/event/report/EventReportForm.vue | Refactor report form to TanStack Form |
| src/app/components/app/radio/AppRadioGroup.vue | Remove form-specific radio item branching |
| src/app/components/app/AppTipTap.vue | Decouple TipTap from Vuelidate BaseValidation |
| src/app/components/app/AppForm.vue | Remove legacy Vuelidate-based form wrapper |
| pnpm-lock.yaml | Lock updates for dependency migration |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/app/components/form/account/registration/FormAccountRegistration.vue
Show resolved
Hide resolved
| <form.Field | ||
| v-slot="{ field: slugField }" | ||
| name="slug" | ||
| :validators="{ | ||
| onBlurAsync: async ({ value: val }) => { | ||
| if (!val) return undefined | ||
|
|
||
| const slugExists = await validateEventSlugFn(val) | ||
| return slugExists | ||
| ? t('validationExistenceNone', { slug: val }) | ||
| : undefined | ||
| }, | ||
| }" | ||
| > | ||
| {{ t('isRemote') }} | ||
| </FormCheckbox> | ||
| </FormInput> | ||
| <!-- <FormInput | ||
| v-if="v$.isInPerson.$model" | ||
| id-label="input-location" | ||
| :placeholder="t('globalPlaceholderAddress').replace('\n', ' ')" | ||
| :title="t('location')" | ||
| type="text" | ||
| :value="v$.location" | ||
| @input="form.location = $event" | ||
| > | ||
| <template #stateError> | ||
| <FormInputStateError | ||
| :form-input="v$.location" | ||
| validation-property="lengthMax" | ||
| > | ||
| {{ t('globalValidationLength') }} | ||
| </FormInputStateError> | ||
| </template> | ||
| <template #stateInfo> | ||
| <FormInputStateInfo> | ||
| {{ t('stateInfoLocation') }} | ||
| </FormInputStateInfo> | ||
| </template> | ||
| </FormInput> --> | ||
| <FormInputUrl :form-input="v$.url" @input="form.url = $event" /> | ||
| <FormInput | ||
| :title="t('description')" | ||
| type="tiptap" | ||
| :value="v$.description" | ||
| @input="form.description = $event" | ||
| > | ||
| <client-only v-if="v$.description"> | ||
| <AppTipTap | ||
| :value="v$.description" | ||
| @input="form.description = $event" | ||
| <FieldError | ||
| v-if="isFieldInvalid(slugField)" | ||
| :errors="normalizeFieldErrors(slugField.state.meta.errors)" | ||
| /> | ||
| </client-only> | ||
| <template #stateError> | ||
| <FormInputStateError | ||
| :form-input="v$.description" | ||
| validation-property="lengthMax" | ||
| </form.Field> |
There was a problem hiding this comment.
The slug field currently has no input/control (only a FieldError), and its uniqueness check is implemented as onBlurAsync. Since there’s nothing to blur, this validator will never run, and on submit only the Zod schema runs (so slug uniqueness is no longer validated). Consider either adding an (even hidden) input and validating onChange/onSubmitAsync, or moving the uniqueness check into a submit-time validator so it always runs before creating/updating events.
383e338 to
ad8e6a6
Compare
This pull request migrates the form handling in the codebase from Vuelidate/Vee-Validate to the new
@tanstack/vue-formlibrary and removes all legacy validation libraries and related dependencies. It also updates the implementation of several form components to use the new validation approach, resulting in significant code simplification and modernization. Additionally, the PR updates thepnpm-lock.yamlfile to reflect these dependency changes.Resolves #1588
Resolves #1672