I'm building an app with Laravel + Inertia + Vue 3 and using Vuelidate for client validation on the "create user" form. Even when I enter the same password into "Password" and "Confirm password", Vuelidate always marks the confirmation as invalid and the form never sends (no POST is observed). The validator error was something like "confirm_password and other must match".
Context / stack:
Backend: Laravel (validation rules in controller).
Frontend: Vue 3 (script setup) + @vuelidate/core + @vuelidate/validators.
Inertia + axios to POST.
Development with Docker Compose (the issue is purely client-side).
Relevant code (summarized):
Laravel controller (for reference): $request->validate([ 'name' => ['required'], 'username' => ['required'], 'email' => ['required', 'email'], 'password' => ['required', 'string', 'min:8', 'confirmed'], 'password_confirmation' => ['required', 'string'], // ... ]);
Vue snippet (rules I originally used): import { sameAs, requiredIf, minLength, required } from '@/Utils/i18n-validators.js'
const rules = { form: { password: { requiredIf: requiredIf(modal.form.change_password || !modal.editMode), minLength: minLength(8) }, password_confirmation: { requiredIf: requiredIf(modal.form.change_password || !modal.editMode), sameAs: sameAs(modal.form.password), // <-- this was the problem: not reactive minLength: minLength(8) }, // ... } }
store() (called when clicking Save; it does not POST if validation fails): const store = () => { v$.value.form.$touch() if (v$.value.form.$invalid) { // show error and DO NOT call axios.post } else { axios.post(route('users.store'), modal.form)... } }
What I tried and discovered:
Using console.log I confirmed modal.form.password and modal.form.password_confirmation contain the same value when typed, but v$.value.form.password_confirmation.$errors still showed an error.
I discovered passing the value directly to sameAs (sameAs(modal.form.password)) causes Vuelidate to capture the value at rule construction time and not re-evaluate when the inputs change.
The fix that worked for me was to pass functions/closures so Vuelidate re-evaluates dynamically: requiredIf: requiredIf(() => modal.form.change_password || !modal.editMode) sameAs: sameAs(() => modal.form.password)
I also converted other requiredIf usages to functions, e.g. requiredIf(() => modal.editMode), requiredIf(() => modal.form.roles.length < 1), etc.
Other details:
The Save button is a PrimaryButton component. At first @click didn't reach the inner
After applying sameAs(() => modal.form.password) and recompiling assets, client validation stopped blocking when the values matched.
Concrete questions:
Is using sameAs(() => modal.form.password) the correct / recommended approach with Vuelidate + Vue 3 so the comparison stays reactive? Is there a better or more idiomatic alternative?
Any recommendations or patterns for organizing dependent rules (requiredIf) with reactive values in setup() so they stay robust and readable?
Practices to avoid overly strict client-side validation blocking submission for optional fields (e.g. reports)? Should some validations be left to the backend only?
Thoughts on temporarily exposing global debug functions (window.__test_store) and using console.log for quick debugging in an Inertia/Vue3/Vuelidate app — acceptable quick practice or is there a better dev workflow?