Displaying validation errors with InertiaJS and Laravel

The easiest way to get yourself accustomed with how inertia works is to stop thinking about inertia visits as being XHR requests where you *do something* when the request it’s completed, and think of your project more as traditional server-side application where everything that happens next is dictated by the server.

The same is true for handling server-side validation errors.

You shouldn’t try to catch() the validation errors and update the form state. Instead, you should validate the form and return the errors as view props, just like you would do with a default Laravel & Blade application.

  1. Submit your form with Inertia.
  2. Perform your regular server-side validation.
  3. If there are any errors, return them as props.

Instead of passing the errors as props for every controller method, you can use the Inertia::share() method in your service provider to include them in every page response:

// AppServiceProvider.php
use Illuminate\Support\Facades\Session;

public function boot()
{ 
    Inertia::share([
        'errors' => function () {
            return Session::get('errors')
                ? Session::get('errors')->getBag('default')->getMessages()
                : (object) [];
        },
    ]);
    // code...
}

Now all your validation errors will be available as props and you can make use of them however you want. For example:

<template>
  <form @submit.prevent="submit">
    <label for="first_name">First name:</label>
    <input id="first_name" v-model="form.first_name" />
    <div v-if="$page.errors.first_name">{{ $page.errors.first_name[0] }}</div>
    <label for="last_name">Last name:</label>
    <input id="last_name" v-model="form.last_name" />
    <div v-if="$page.errors.last_name">{{ $page.errors.last_name[0] }}</div>
    <label for="email">Email:</label>
    <input id="email" v-model="form.email" />
    <div v-if="$page.errors.email">{{ $page.errors.email[0] }}</div>
    <button type="submit">Submit</button>
  </form>
</template>

The only problem with this approach is that it does not support having two or more forms having the same input names.

When the validation fails, the error is displayed for every input field having that name.

Both forms display the error, but only the first was submitted.

Named error bags

If you have multiple forms on a single page, you may need to name your error bags differently in order to prevent displaying the same error message for fields having the same name.

To do so, you can use the validateWithBag method for validating inside the controller, while for form request objects you can override the $errorBag property.

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateJobInspectionRequest extends FormRequest
{
    protected $errorBag = 'updateJobInspection'

    // You can tap in here if you need really dynamic names 
    public function prepareForValidation()
    {
        $this->errorBag = "jobInspection.{$this->route('jobInspection')->id}";
    }

    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'status' => 'required',
            'name' => 'required'
        ];
    }
}

Since we’re now using named error bags in some parts of our application, we should also update the AppServiceProvider to include all the error bags, not only the default one:

Inertia::share('errors', function () {
    if (Session::get('errors')) {
        $bags = [];

        foreach (Session::get('errors')->getBags() as $bag => $error) {
            $bags[$bag] = $error->getMessages();
        }

        return $bags;
    }

    return (object)[];
});

This also means we won’t be able to use $page.errors.name[0] anymore.

What I like to do is to create a vue mixin with an error(field, errorBag) method that takes the field name as the first parameter and the error bag name as the second – and then I can use it anywhere in my application.

// app.js
Vue.mixin({
  methods: {
    error(field, errorBag = 'default') {
      if (!this.$page.errors.hasOwnProperty(errorBag)) {
        return null;
      }

      if (this.$page.errors[errorBag].hasOwnProperty(field)) {
        return this.$page.errors[errorBag][field][0];
      }

      return null;
    }
  }
});

The usage looks something like this:

<template>
  <form @submit.prevent="submit">
    <label for="first_name">First name:</label>
    <input id="first_name" v-model="form.first_name" />
    <div v-if="error('first_name')">{{ error('first_name') }}</div>
    <label for="last_name">Last name:</label>
    <input id="last_name" v-model="form.last_name" />
    <div v-if="error('last_name')">{{ error('last_name') }}</div>
    <label for="email">Email:</label>
    <input id="email" v-model="form.email" />
    <div v-if="error('email')">{{ error('email') }}</div>
    <button type="submit">Submit</button>
  </form>
</template>

Subscribe to get my latest blog posts.