Skip to content

Conversation

@pascalbaljet
Copy link
Member

@pascalbaljet pascalbaljet commented Oct 3, 2025

This PR brings support for Laravel Precognition to the <Form> component. We decided to build this directly into Inertia (instead of the Precognition library) for two reasons:

  • While patching useForm() in laravel-precognition is manageable, patching the <Form> component would quickly become messy and difficult to unit test. Inertia already has an extensive Playwright setup.
  • It's just two additional headers sent to the backend. Laravel already supports this, and it should be fairly easy for other frameworks to implement. We'll update our protocols page once this PR is merged.

Here's how you can use it in Vue:

<template>
  <Form action="/users" method="post" #default="{ errors, invalid, validate, validating }">
    <div>
      <input name="name" @change="validate('name')" />
      <p v-if="invalid('name')"> {{ errors.name }} </p>
    </div>

    <div>
      <input name="email" @change="validate('email')" />
      <p v-if="invalid('email')"> {{ errors.email }} </p>
    </div>

    <p v-if="validating">Validating...</p>
  </Form>
</template>

In addition to invalid(), there's also valid(), which returns true only when the field has been validated and contains no errors.

<template>
  <Form action="/users" method="post" #default="{ errors, invalid, valid, validate }">
    <div>
      <input name="name" @change="validate('name')" />
      <p v-if="valid('name')"> ✅ Name has been validated </p>
      <p v-if="invalid('name')"> ❌ Name is invalid </p>
    </div>
  </Form>
</template>

You may configure the debounce timeout (defaults to 1500ms) and include files with the validateFiles and validateTimeout props.

<template>
  <Form action="/documents" method="post" validate-files :validate-timeout="500">
    <!-- ... -->
  </Form>
</template>

A touch() method is available to mark fields for validation, and a touched() method checks if fields have been touched. The existing reset() method has been updated to also reset the touched state of a field.

<template>
  <Form
    action="/form-component/precognition"
    method="post"
    #default="{ errors, invalid, reset, touch, validate }"
  >
    <div>
      <input name="name" />
      <p v-if="invalid('name')"> {{ errors.name }} </p>
    </div>

    <div>
      <input name="email" />
      <p v-if="invalid('email')"> {{ errors.email }} </p>
    </div>

    <button type="button" @click="reset()">Reset All</button>
    <button type="button" @click="reset('name')">Reset Name</button>
    <button type="button" @click="reset(['name', 'email'])">Reset Name and Email</button>

    <button type="button" @click="touch('name')">Touch Name</button>
    <button type="button" @click="touch(['name', 'email'])">Touch Name and Email</button>

    <p v-if="touched()">One or more fields have been touched</p>
    <p v-if="touched('name')">Name has been touched</p>

    <button type="button" @click="validate()">Validate touched fields</button>  
    <button type="button" @click="validate(['name', 'email'])">Validate multiple fields</button>  
  </Form>
</template>

The defaults() method also has been updated to sets the current state of the data on the internal validator, ensuring it only triggers validation when it changes again.

<template>
  <Form
    action="/users"
    method="post"
    #default="{ defaults }"
  >
    <input name="name" value="John" />
    <input name="email" value="[email protected]" />

    <button type="button" @click="defaults()">
      Set Current Values as Defaults
    </button>
  </Form>
</template>

Lastly, you may pass an object of options to validate().

<template>
  <Form action="/form-component/precognition" method="post" #default="{ validate }">
    <input name="name" />

    <button
      type="button"
      @click="
        validate('name', {
          onPrecognitionSuccess: () => {
            // ...
          },
          onValidationError: () => {
            // ...
          },
          onFinish: () => {
            // ...
          },
        })
      "
    >
      Validate Name with Callbacks
    </button>
  </Form>
</template>

Alternatively, you may pass an only option to the options object, which can be a string or array of fields.

validate({
  only: 'name',
  onValidationError: () => {
    // ...
  }
})

The onBefore option allow you to hook into the validation lifecycle. onBefore receives the new and old request data, and returning false will prevent the validation request.

validate('name', {
  onBefore: (newRequest, oldRequest) => {
    // newRequest: { data: {...}, touched: ['name'] }
    // oldRequest: { data: {...}, touched: [...] }

    // Return false to prevent validation
    if (someCondition) {
      return false
    }
  },
})

Laravel validation errors are typically arrays of messages per field. The Inertia middleware automatically simplifies these to just the first error for each field. However, the middleware doesn't run on 422 responses, so the <Form> component handles this transformation by default. Set simple-validation-errors to false to keep the original array format.

<template>
  <Form action="/users" method="post">
    <!-- errors.name will be a string: "The name field is required" -->
  </Form>

  <!-- Original format - keeps full error arrays -->
  <Form action="/users" method="post" :simple-validation-errors="false">
    <!-- errors.name will be an array: ["The name field is required", "The name must be at least 3 characters"] -->
  </Form>
</template>

@pascalbaljet pascalbaljet marked this pull request as ready for review October 14, 2025 15:43
@axelvaindal
Copy link

Hello @pascalbaljet 👋

Sorry for the hop-in, looks like this PR is solving frontend form validation issue we (and others) have while using complex form with Inertia.

Do you know when this would land production ? 🙏

@pascalbaljet pascalbaljet marked this pull request as draft November 6, 2025 17:17
@pascalbaljet
Copy link
Member Author

Converted back to draft as I want to refactor the integration to be based on PR #2684.

Do you know when this would land production ? 🙏

I can't give an exact date, but probably in 2-3 weeks.

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.

3 participants