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

jmacdiarmid's avatar

Livewire form validation - conditional validation

This is a Laravel 8 project and I'm in the process if building the user management module. I have name, email, phone, password and role. I would like to validate the phone field if there is a value otherwise it's nullable. Similarly, with the password field. require when creating a new user. ignore it when in edit mode and password is not to be changed.

Here's my backend code:

namespace App\Http\Livewire\Admin;

use App\Models\User;
use Hash;
use Livewire\Component;
use Livewire\WithPagination;
use Log;
use Spatie\Permission\Models\Role;

class Users extends Component
{

    use WithPagination;

    public int $currentPage = 1;
    public int $perPage = 10;
    public string $success = '';

    public bool $showModal = false;

    public $authUser;

    public $userId;
    public $user;
    public $name;
    public $phone;
    public $email;
    public $password;
    public $password_confirmation;
    public $roles = [];
    public $selectedRole;

    protected $rules = [
        'name'          => 'required|string|max:255',
        'email'         => 'required|string|email|max:255|unique:users',
        'password'      => 'required_if:exists()|confirmed|min:8',
        'selectedRole'  => 'required|string',
    ];

    public function mount(): void
    {
        $this->roles = Role::all();
        $this->authUser = Auth()->user();
    }

    public function updated(string $propertyName): void
    {
        $this->validateOnly($propertyName, $this->rules);
    }

    public function create(): void
    {
        $this->showModal = true;

        $this->resetErrorBag();

        $this->resetValidation();

        $this->user = null;

        $this->userId = null;
    }

    /**
     * @throws \JsonException
     */
    public function edit($userId): void
    {
        try {

            $this->showModal = true;

            $this->userId = $userId;

            $user = User::findOrFail($userId);

            $this->name = $user->name;
            $this->email = $user->email;
            $this->phone = $user->phone;
            $this->selectedRole = $user->selectedRole;

        } catch (\Exception $ex) {
            $error = sprintf('[%s],[%d] ERROR:[%s]', __METHOD__, __LINE__,
                json_encode($ex->getMessage(), JSON_THROW_ON_ERROR | true));
            Log::error($error);
        }
    }

    public function store(): void
    {
            $validatedData = $this->validate($this->rules);

            $validatedData['password'] = Hash::make($validatedData['password']);

            $user = User::updateOrcreate(['id' => $this->userId], $validatedData);

            $user->assignRole($validatedData['selectedRole']);

            $this->reset();
            $this->resetValidation();

            $updateOrCreateVerb = ($this->userId) ? 'updated' : 'created';

            $this->success = "User {$user->name} was {$updateOrCreateVerb} successfully!";

            $this->close();

    }

    public function show($userId): void
    {
        $this->reset();

        $this->showModal = true;

        $this->userId = $userId;

        $this->user = User::find($userId);
    }

    public function delete($userId): void
    {
        try {

            $user = User::find($userId);

            if ($user) {
                $user->delete();
            }

        } catch (\Exception $ex) {
            Log::error($ex->getMessage());
        }
    }

    public function close(): void
    {
        $this->showModal = false;
    }

    public function resetSuccess(): void
    {
        $this->reset('success');
    }

    public function render()
    {
        return view('livewire.admin.users', [
            'roles' => $this->roles,
            'users' => User::with('roles')->latest()->orderBy('id', 'ASC')->paginate($this->perPage),
        ]);
    }
}

Here's my view code:

<div>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('User Management') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-6xl mx-auto sm:px-6 lg:px-8">
            <div class="inline-block mb-5">
                <button wire:click.prevent="create" type="button" class="bg-indigo-500 hover:bg-indigo-600 text-white text-sm
	    	    py-2.5 px-5 rounded-md transition duration-500 ease-in-out transform hover:-translate-y-1 shadow-l">
                    Create New User
                </button>
            </div>
            <div class="inline-block min-w-full shadow overflow-hidden">
                <table class="min-w-full leading-normal">
                    <thead>
                    <tr>
                        <th class="px-3 py-3 border-b-2 border-black bg-black text-left text-xs font-semibold text-white uppercase tracking-wider">
                            {{ __('Name') }}
                        </th>
                        <th class="px-3 py-3 border-b-2 border-black bg-black text-left text-xs font-semibold text-white uppercase tracking-wider">
                            {{ __('Email') }}
                        </th>
                        <th class="px-3 py-3 border-b-2 border-black bg-black text-left text-xs font-semibold text-white uppercase tracking-wider">
                            {{ __('Phone') }}
                        </th>
                        <th class="px-3 py-3 border-b-2 border-black bg-black text-left text-xs font-semibold text-white uppercase tracking-wider">
                            {{ __('Roles') }}
                        </th>
                        <th class="px-5 py-3 border-b-2 border-black bg-black text-left text-xs font-semibold text-white uppercase tracking-wider">
                        </th>
                    </tr>
                    </thead>
                    <tbody>
                    @forelse ($users as $user)
                        <tr>
                            <td class="px-5 py-2 bg-white text-sm @if (!$loop->last) border-gray-200 border-b @endif">
                                {{ $user->name }}
                            </td>
                            <td class="px-5 py-2 bg-white text-sm @if (!$loop->last) border-gray-200 border-b @endif">
                                {{ $user->email }}
                            </td>
                            <td class="px-5 py-2 bg-white text-sm @if (!$loop->last) border-gray-200 border-b @endif">
                                {{ $user->phone }}
                            </td>
                            <td class="px-5 py-2 bg-white text-sm @if (!$loop->last) border-gray-200 border-b @endif">
                                @forelse($user->getRoleNames() as $roleName)
                                    <span class="inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-green-100 bg-green-600 rounded-full">
                					    {{ $roleName }}
                                    </span>
                                @empty
                                    {{ __('No Roles Found') }}
                                @endforelse
                            </td>
                            <td class="px-5 py-2 bg-white text-sm @if (!$loop->last) border-gray-200 border-b @endif text-right">
                                <div class="inline-block whitespace-no-wrap">
                                    <button wire:click.prevent="show({{ $user->id }})" type="button" class="bg-green-500 hover:bg-green-700 text-white font-bold px-4 py-1 rounded">
				    	<x-icons.eyeball/>
                                    </button>
                                    <button wire:click.prevent="edit({{ $user->id }})" type="button" class="bg-blue-500 hover:bg-blue-700 text-white font-bold px-4 py-1 rounded">
                                        <x-icons.editpad />
		                    </button>
                                    <button wire:click.prevent="$emit('triggerDelete',{{ $user }})" class="bg-red-500 hover:bg-red-700 text-white font-bold px-4 py-1 rounded">
                                        <x-icons.trashcan />
                                    </button>
                                </div>
                            </td>
                        </tr>
                    @empty
                        <tr>
                            <td>
                                {{ __('No Users Found') }}
                            </td>
                        </tr>
                    @endforelse
                    </tbody>
                </table>
            </div>
            {{ $users->links() }}
        </div>
    </div>

    <div class="@if (!$showModal) hidden @endif flex items-center justify-center fixed left-0 bottom-0 w-full  h-full bg-gray-800 bg-opacity-90">

        {{--  Modal  --}}
        <div class="bg-white rounded-lg w-2/5">

            @if ($errors->any())
            <div class="relative m-4 px-4 py-3 text-sm bg-red-100 border border-red-400 text-red-700  rounded" role="alert">
                <strong class="font-bold">Oops!</strong><span class="block sm:inline">There are some errors with your submission.</span>
            </div>
            @endif

            @if ($success)
            <div class="relative m-4 px-4 py-3 text-sm bg-green-100 border border-green-400 text-green-700 rounded" role="alert">
                <span class="block sm:inline">{{ $success }}</span>
                <span wire:click="resetSuccess" class="absolute top-0 bottom-0 right-0 px-4 py-3">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                    </svg>
                </span>
            </div>
            @endif

            <form wire:submit.prevent="store">
                @csrf
                <div class="flex flex-col items-start p-4">
                    <div class="flex items-center w-full mb-4">
                        <div class="text-gray-900 font-medium text-lg">{{ $userId ? 'Edit User' : 'Create New User' }}</div>
                        <svg wire:click="close" class="ml-auto fill-current text-gray-700 w-6 h-6 cursor-pointer" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
                            <path d="M14.53 4.53l-1.06-1.06L9 7.94 4.53 3.47 3.47 4.53 7.94 9l-4.47 4.47 1.06 1.06L9 10.06l4.47 4.47 1.06-1.06L10.06 9z"/>
                        </svg>
                    </div>

                    <div class="w-full">
                        <label class="block font-semibold text-sm text-gray-700" for="name">Name</label>
                        <input wire:model.defer="name" id="name" name="name" type="text"
                               class="text-sm sm:text-base rounded-lg border border-gray-400 w-full pl-2 pr-4 py-2 mb-2 focus:outline-none focus:border-blue-400"
                               autocomplete="name"/>
                        @error('name') <span class="text-xs text-red-500 mt-1">{{ $message }}</span> @enderror
                    </div>

                    <div class="w-full">
                        <div class="flex flex-wrap -mx-3 sm:-mx-4 md:-mx-4 lg:-mx-4 xl:-mx-4 overflow-hidden">
                            <div class="my-2 px-3 w-1/2 sm:my-2 sm:px-4 sm:w-1/2 md:my-2 md:px-4 md:w-1/2 lg:my-2 lg:px-3 lg:w-1/2 xl:my-2 xl:px-4 xl:w-1/2 overflow-hidden">
                                <label class="block font-semibold text-sm text-gray-700" for="email">Email</label>
                                <input wire:model.defer="email" id="email" name="email" type="email"
                                       class="text-sm sm:text-base rounded-lg border border-gray-400 w-full pl-2 pr-4 py-2 focus:outline-none focus:border-blue-400"
                                       autocomplete="email"/>
                                @error('email') <span class="text-xs text-red-500 mt-1">{{ $message }}</span> @enderror
                            </div>
                            <div class="my-2 px-3 w-1/2 sm:my-2 sm:px-2 sm:w-1/2 md:my-2 md:px-4 md:w-1/2 lg:my-2 lg:px-3 lg:w-1/2 xl:my-2 xl:px-4 xl:w-1/2 overflow-hidden">
                                <label class="block font-semibold text-sm text-gray-700" for="phone">Phone</label>
                                <input wire:model.defer="phone" id="phone" name="guard_name" type="tel"
                                       class="text-sm sm:text-base rounded-lg border border-gray-400 w-full pl-2 pr-4 py-2 focus:outline-none focus:border-blue-400"/>
                                @error('phone') <span class="text-xs text-red-500 mt-1">{{ $message }}</span> @enderror
                            </div>
                        </div>
                    </div>

                    <div class="w-full">
                        <div class="flex flex-wrap -mx-3 sm:-mx-4 md:-mx-4 lg:-mx-4 xl:-mx-4 overflow-hidden">
                            <div class="my-2 px-3 w-1/2 sm:my-2 sm:px-4 sm:w-1/2 md:my-2 md:px-4 md:w-1/2 lg:my-2 lg:px-3 lg:w-1/2 xl:my-2 xl:px-4 xl:w-1/2 overflow-hidden">
                                <label class="block font-semibold text-sm text-gray-700" for="password">Password</label>
                                <input wire:model.defer="password" id="password" name="password" type="password"
                                       class="text-sm sm:text-base rounded-lg border border-gray-400 w-full pl-2 pr-4 py-2 focus:outline-none focus:border-blue-400"/>
                                @error('password') <span class="text-xs text-red-500 mt-1">{{ $message }}</span> @enderror
                            </div>
                            <div class="my-2 px-3 w-1/2 sm:my-2 sm:px-4 sm:w-1/2 md:my-2 md:px-4 md:w-1/2 lg:my-2 lg:px-3 lg:w-1/2 xl:my-2 xl:px-4 xl:w-1/2 overflow-hidden">
                                <label class="block font-semibold text-sm text-gray-700" for="password_confirmation">Password Confirmation</label>
                                <input wire:model.defer="password_confirmation" id="password_confirmation" name="password_confirmation" type="password"
                                       class="text-sm sm:text-base rounded-lg border border-gray-400 w-full pl-2 pr-4 py-2 focus:outline-none focus:border-blue-400"/>
                                @error('password_confirmation') <span class="text-xs text-red-500 mt-1">{{ $message }}</span> @enderror
                            </div>
                        </div>
                    </div>

                    <div class="w-full">
                        <label class="block mt-4 mb-2 font-bold text-sm text-gray-700" for="roles">Roles</label>
                        <div class="grid grid-cols-5 grid-rows-2 gap-1 lg:grid-cols-5">

                        @foreach($roles as $role)
                            <label for="roles">
                                <input wire:model.defer="selectedRole" id="selectedRole" name=selectedRole" class="form-checkbox" type="radio" value="{{ $role->name }}" @if ( old('selectedRole', $this->selectedRole)) checked @endif>
                                <span class="ml-2 font-bold">{{ $role->name }}</span>
                            </label>
                        @endforeach

                        </div>
                        @error('roles') <span class="text-xs text-red-500 mt-1">{{ $message }}</span> @enderror
                    </div>

                    <div class="ml-auto mt-5">
                        <button wire:click="close" type="button" class="bg-gray-500 text-white font-bold py-2 px-4 rounded" data-dismiss="modal">
                            Close
                        </button>
                        <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
                            {{ $userId ? 'Save Changes' : 'Save' }}
                        </button>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>

@push('styles')
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@10/dist/sweetalert2.min.css">
@endpush

@push('scripts')
    <script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
    <script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.js"></script>
    <script type="text/javascript">
        document.addEventListener('DOMContentLoaded', function () {

            @this.on('triggerDelete', userId => {
                Swal.fire({
                    title: 'Are You Sure?',
                    text: 'User record will be deleted!',
                    type: "warning",
                    showCancelButton: true,
                    confirmButtonColor: '#d33',
                    cancelButtonColor: '#3085d6',
                    confirmButtonText: 'Delete!'
                }).then((result) => {
                    if (result.value) {
                        @this.call('delete',userId)
                    } else {
                        console.log("Canceled");
                    }
                });
            });
        })
    </script>
@endpush
0 likes
3 replies
SilenceBringer's avatar

@jmacdiarmid with phone it's easy, just prepend nullable to field validation https://laravel.com/docs/8.x/validation#rule-nullable

    protected $rules = [
        // ...
        'phone'         => 'nullable|otherRules',
    ];

this way it will accept null value or validate input with other rules

with password it's little more tricky, as far as it depends of the $userId. For now I can see 2 solutions:

  1. create separated component for add and edit, and extends 1 main component (with all the functions), but with different rules
  2. (not elegant, but should works) change validation rules just before performing validation, like
public function store(): void
    {
            $rules['password'] = $this->userId
                ? 'nullable|confirmed|min:8'
                : 'required|confirmed|min:8';

            $validatedData = $this->validate($this->rules);
1 like
jmacdiarmid's avatar

Thanks for the suggestion! I'll try both and see which works the best.

prospero's avatar

You can improve taking all in the same create/edit modal if is like you have it. Generally, when create new user you need to fill all the required data, could be email and password, or however you want manage like required. But in edit, not necessary you want to change the password, instead other fields. You can handle this showing always the password field on create, but conditionally on edit ( like a "Reset Password" checkbox). That way, on edit you can build the validation rules (Validator::make) with password property or not

Please or to participate in this conversation.