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. The goal is to provide complete feature parity with the Laravel Precognition library. We decided to build this directly into Inertia for three reasons:

  • We don't want to make laravel-precognition an optional peer dependency of Inertia.
  • 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="john@example.com" />

    <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', {
          onSuccess: () => {
            // ...
          },
          onError: (errors) => {
            // ...
          },
          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',
  onError: () => {
    // ...
  }
})

The onBeforeValidation and onException options allow you to hook into the validation lifecycle. onBeforeValidation receives the new and old request data, and returning false will prevent the validation request. onException handles non-422 errors that occur during validation.

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

    // Return false to prevent validation
    if (someCondition) {
      return false
    }
  },
  onException: (error) => {
    // Handle non-422 validation errors (network errors, 500s, etc.)
    console.error('Validation request failed:', error)
  },
})

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>

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.

1 participant