Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

garrettmassey's avatar

Laravel + Inertia + Vue: Rendering Form Errors

I am making some edits to the Breeze register functionality, and requiring a phone number instead of an email address. I use a Phone number input component that provides me with various formats for the phone number after input.

Currently, we only want to support US phone numbers (phone numbers starting with 1 like so: 15555555555)

The form validation errors work for the first and last name inputs, but if there are any errors on the phone input, the form doesn't submit, but the errors don't show up either, and I'm not sure why?

Register.vue:

<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import {Head, Link, useForm} from '@inertiajs/vue3';
import {ref} from "vue";
import MazPhoneNumberInput from 'maz-ui/components/MazPhoneNumberInput';

const form = useForm({
    first_name: '',
    last_name: '',
    phone: ref({}),
});

const submit = () => {
    form.post(route('register'), {
        onFinish: () => form.reset('first_name', 'last_name', 'phone'),
    });
};
</script>

<template>
    <GuestLayout>
        <Head title="Register"/>
        <form @submit.prevent="submit">
            <div>
                <InputLabel for="first_name" value="First Name"/>
                <TextInput
                    id="first_name"
                    type="text"
                    class="mt-1 block w-full"
                    v-model="form.first_name"
                    autofocus
                    autocomplete="first_name"
                />
                <InputError class="mt-2" :message="form.errors.first_name"/>
            </div>
            <div>
                <InputLabel for="last_name" value="Last Name"/>
                <TextInput
                    id="last_name"
                    type="text"
                    class="mt-1 block w-full"
                    v-model="form.last_name"
                    required
                    autocomplete="last_name"
                />
                <InputError class="mt-2" :message="form.errors.last_name"/>
            </div>
            <div class="mt-4">
                <InputLabel for="phone" value="Phone Number"/>

                <MazPhoneNumberInput
                    id="phone"
                    color="info"
                    :only-countries="['US']"
                    :no-country-selector="true"
                    @update="form.phone = $event"
                    :success="form.phone?.isValid"
                />
                <InputError v-if="form.errors.phone" class="mt-2" :message="form.errors.phone"/>
            </div>
            <div class="flex items-center justify-end mt-4">
                <Link
                    :href="route('login')"
                class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none     focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                >
                    Already registered?
                </Link>
                <PrimaryButton
                    class="ml-4"
                :class="{ 'opacity-25': ((form.processing || !results?.isValid) && (form.first_name == null     && form.last_name == null)) }"
                :disabled="(form.processing || !results?.isValid) && (form.first_name == null &&     form.last_name == null)"
                >Sign up
                </PrimaryButton>
            </div>
        </form>
    </GuestLayout>
</template>

RegisteredUserController::store()

public function store(RegisterRequest $request): RedirectResponse
{
    $data = $request->validated();

    ddd($data);
    /* ... rest of code */
}

RegisterRequest (Form Request):

<?php

namespace App\Http\Requests\Auth;

use Illuminate\Foundation\Http\FormRequest;
use JetBrains\PhpStorm\ArrayShape;

class RegisterRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'first_name' => ['required', 'string', 'max:255'],
            'last_name' => ['required', 'string', 'max:255'],
            'phone.e164' => ['required', 'unique:users,phone_e164'],
            'phone.countryCallingCode' => ['required', 'numeric', 'digits:1', 'in:1'],
            'phone.countryCode' => ['required', 'string', 'max:2'],
            'phone.formatInternational' => ['required', 'string'],
            'phone.formatNational' => ['required', 'string'],
            'phone.isPossible' => ['required', 'boolean'],
            'phone.isValid' => ['required', 'boolean'],
            'phone.nationalNumber' => ['required', 'numeric', 'digits:10'],
            'phone.rfc3966' => ['required', 'string'],
            'phone.type' => ['required', 'string'],
            'phone.uri' => ['required', 'string'],
        ];
    }

    #[ArrayShape(['phone.e164.unique' => 'string', 'phone.countryCallingCode.in' => 'string'])]
    public function messages(): array
    {
        return [
            'phone.e164.unique' => 'This phone number is already registered. Please log in.',
            'phone.countryCallingCode.in' => 'Sorry, we only support US phone numbers at this time.',
        ];
    }
}

I'm not sure why the validation errors for the phone are not showing up on the Vue Register page. The errors are being caught because we're never reaching the ddd() method, but the errors aren't showing up on Vue either.

Edit: I pasted the Form request code twice. Updated with the correct code.

0 likes
5 replies
garrettmassey's avatar

@undeportedmexican

Here is the Register.vue page:

<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import {Head, Link, useForm} from '@inertiajs/vue3';
import {ref} from "vue";
import MazPhoneNumberInput from 'maz-ui/components/MazPhoneNumberInput';

const form = useForm({
    first_name: '',
    last_name: '',
    phone: ref({}),
});

const submit = () => {
    form.post(route('register'), {
        onFinish: () => form.reset('first_name', 'last_name', 'phone'),
    });
};
</script>

<template>
    <GuestLayout>
        <Head title="Register"/>
        <form @submit.prevent="submit">
            <div>
                <InputLabel for="first_name" value="First Name"/>
                <TextInput
                    id="first_name"
                    type="text"
                    class="mt-1 block w-full"
                    v-model="form.first_name"
                    autofocus
                    autocomplete="first_name"
                />
                <InputError class="mt-2" :message="form.errors.first_name"/>
            </div>
            <div>
                <InputLabel for="last_name" value="Last Name"/>
                <TextInput
                    id="last_name"
                    type="text"
                    class="mt-1 block w-full"
                    v-model="form.last_name"
                    required
                    autocomplete="last_name"
                />
                <InputError class="mt-2" :message="form.errors.last_name"/>
            </div>
            <div class="mt-4">
                <InputLabel for="phone" value="Phone Number"/>

                <MazPhoneNumberInput
                    id="phone"
                    color="info"
                    :only-countries="['US']"
                    :no-country-selector="true"
                    @update="form.phone = $event"
                    :success="form.phone?.isValid"
                />
                <InputError v-if="form.errors.phone" class="mt-2" :message="form.errors.phone"/>
            </div>
            <div class="flex items-center justify-end mt-4">
                <Link
                    :href="route('login')"
                class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none     focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                >
                    Already registered?
                </Link>
                <PrimaryButton
                    class="ml-4"
                :class="{ 'opacity-25': ((form.processing || !results?.isValid) && (form.first_name == null     && form.last_name == null)) }"
                :disabled="(form.processing || !results?.isValid) && (form.first_name == null &&     form.last_name == null)"
                >Sign up
                </PrimaryButton>
            </div>
        </form>
    </GuestLayout>
</template>
undeportedmexican's avatar

@garrettmassey as Mohammed mentioned, you're not properly requesting the valiation errors correclty, you can reduce the array as he mentions to gather all the errors, that solves your question.

As for practicality, I don't think you should be concerned with all those errors, as you only have messages for e164.unique and phone.countryCallingCode.In, so I really would remove the required rule from the rest and focus only on the 2 errors you have messages for, accessing them with form.errors.phone.e164 and form.errors.phoneCountryCallingCode.in

1 like
MohamedTammam's avatar
Level 51

You're trying to display form.errors.phone, but you will not get that. Instead you will get, form.errors.phone.e164, form.errors.phone.countryCallingCode, form.errors.phone.countryCode`, etc.

You need to modify your logic for this. Something like the following:

<script setup>
// ...
const phoneErrors = computed(() => {
	const phoneProps = ['e164', 'countryCallingCode', 'countryCode', ...etc];
	
	return phoneProps .reduce((prev, curr) => {
		if(form.errors.phone && form.errors.phone[curr])
			prev.push(form.errors.phone[curr][0]);
		return prev;
	}, []);
});
// ...
</script>

<template>
<!-- .... -->

<!-- Displaying phone errors-->
<div v-for="error in phoneErrors">{{ error }}</div>

<!-- .... -->
</template>
garrettmassey's avatar

@MohamedTammam

Thank you! I didn't realize I needed to iterate through each specific error, I was under the impression that Laravel would bundle the errors for phone.* together.

1 like

Please or to participate in this conversation.