Back to Course |
Laravel Array Validation: All You Need To Know

Livewire: Real-Time Array Validation

Now that we looked at static multi-dimension forms, it would be cool to have them dynamic. We can use Livewire to add and remove fields from the form. As a bonus, it even handles real-time validation for us.


Livewire Component

To add dynamic elements, we need to create a Livewire component. We can do that with the following command:

php artisan make:livewire TeamPlayerSelection

We'll modify the TeamPlayerSelection component to look like this:

 
use App\Models\Team;
use App\Models\User;
use Livewire\Component;
 
class TeamPlayerSelection extends Component
{
// Team property used for Edit
public ?Team $team = null;
// Public array of users for our select dropdown
public array $users = [];
// Public array of positions for our select dropdown
public array $positions = [
'Goalkeeper' => 'Goalkeeper',
'Defender' => 'Defender',
'Midfielder' => 'Midfielder',
'Forward' => 'Forward',
'Coach' => 'Coach',
'Assistant Coach' => 'Assistant Coach',
'Physiotherapist' => 'Physiotherapist',
'Doctor' => 'Doctor',
'Manager' => 'Manager',
'President' => 'President',
];
// These are the players we are going to save. We'll use this in our validation rules
public array $players = [];
 
// This is the template for a new user with empty defaults
public array $userTemplate = [
'user_id' => "",
'position' => ""
];
 
// Validation rules following the array structure
protected $rules = [
'players' => ['required', 'array', 'min:3', 'max:10'],
'players.*.id' => ['required', 'exists:users,id', 'integer', 'distinct'],
'players.*.position' => ['required', 'string', 'max:200', 'distinct'],
];
 
// Custom messages for validation rules to make them more user-friendly
protected $messages = [
'players.*.id.required' => 'The player field is required.',
'players.*.id.exists' => 'The selected player is invalid.',
'players.*.id.distinct' => 'Player cannot be selected twice',
'players.*.position.required' => 'Player position is required',
'players.*.position.distinct' => 'Player positions must be unique',
];
 
// On update, we want to re-validate the whole component
public function updated($propertyName)
{
$this->validate();
}
 
public function mount()
{
// We load all users in the select dropdown
$this->users = User::pluck('name', 'id')->toArray();
 
// If we have any old input, we'll load it into our players' array
if (count(old('players', [])) > 0) {
foreach (old('players', []) as $index => $player) {
$this->players[$index] = [
'id' => $player['id'],
'position' => $player['position']
];
}
} elseif ($this->team) {
// Otherwise, if we are editing a team, we'll load the players from the database
$this->team->load(['users']);
foreach ($this->team->users as $index => $player) {
$this->players[$index] = [
'id' => $player->id,
'position' => $player->pivot->position
];
}
}
}
 
public function render()
{
return view('livewire.team-player-selection');
}
 
// This adds a new line to our players' array and re-validates the component
public function addUser(): void
{
$this->players[] = $this->userTemplate;
$this->validate();
}
 
// This removes a line from our players' array and re-validates the component
public function removeUser($index)
{
unset($this->players[$index]);
$this->validate();
}
}

And our view will look like this:

<div>
@foreach($players as $i => $user)
<div class="mb-4 grid grid-cols-3 gap-4">
<div class="">
<label class="text-xl text-gray-600" for="players[{{$i}}][id]">Player<span
class="text-red-500">*</span></label>
<select name="players[{{$i}}][id]" id="players[{{$i}}][id]"
wire:model="players.{{$i}}.id"
class="border-2 border-gray-300 p-2 w-full">
<option value="">Select Player</option>
@foreach($users as $id => $name)
<option value="{{ $id }}"
@if(in_array($id, collect($players)->filter(fn($value, $key) => $key != $i)->pluck('id')->toArray()))
disabled
@endif
@selected(old('players.'.$i.'.id') == $id)
>{{ $name }}</option>
@endforeach
</select>
@error('players.'.$i.'.id')
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
<div class="">
<label class="text-xl text-gray-600" for="players[{{$i}}][position]">Position<span
class="text-red-500">*</span></label>
<select name="players[{{$i}}][position]" id="players[{{$i}}][position]"
wire:model="players.{{$i}}.position"
class="border-2 border-gray-300 p-2 w-full">
<option value="">Select Position</option>
@foreach($positions as $id => $name)
<option value="{{ $id }}"
@if(in_array($id, collect($players)->filter(fn($value, $key) => $key != $i)->pluck('position')->toArray()))
disabled
@endif
@selected(old('players.'.$i.'.position') == $id)
>{{ $name }}</option>
@endforeach
</select>
@error('players.'.$i.'.position')
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
<div class="">
<button wire:click.prevent="removeUser({{$i}})"
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded mt-8">
Remove
</button>
</div>
</div>
@endforeach
 
<div class="mb-4">
@error('players')
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
 
<button wire:click.prevent="addUser"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mb-4">
Add Player
</button>
</div>

Here, we have a few things to note:

We are using a wire:click to trigger an addUser() function in our component:

<button wire:click.prevent="addUser"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mb-4">
Add Player
</button>

The exact use case for the removal of a player with removeUser($index):

<button wire:click.prevent="removeUser({{$i}})"
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded mt-8">
Remove
</button>

And as a bonus with Livewire, we are disabling the select options that are already selected in the other lines:

{{-- ... --}}
 
@if(in_array($id, collect($players)->filter(fn($value, $key) => $key != $i)->pluck('id')->toArray()))
disabled
@endif
 
{{-- ... --}}
 
@if(in_array($id, collect($players)->filter(fn($value, $key) => $key != $i)->pluck('position')->toArray()))
disabled
@endif
 
{{-- ... --}}

Using the Component

Using the component is pretty simple, and we need to call it on our form:

resources/views/teams/create.blade.php

<form method="POST" action="{{ route('teams.store') }}">
@csrf
<div class="mb-4">
<label class="text-xl text-gray-600" for="name">Name <span
class="text-red-500">*</span></label>
<input type="text" class="border-2 border-gray-300 p-2 w-full" name="name" id="name"
value="{{ old('name') }}">
@error('name')
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
 
<livewire:team-player-selection/>
 
<div class="mb-4">
<button type="submit"
class="bg-blue-500 text-white px-4 py-2 rounded font-medium">Create Team
</button>
</div>
</form>

And in the edit, we need to pass the team to the component:

resources/views/teams/edit.blade.php

<form method="POST" action="{{ route('teams.update', $team->id) }}">
@csrf
@method('PUT')
<div class="mb-4">
<label class="text-xl text-gray-600" for="name">Name <span
class="text-red-500">*</span></label>
<input type="text" class="border-2 border-gray-300 p-2 w-full" name="name" id="name"
value="{{ old('name', $team->name) }}">
@error('name')
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
 
<livewire:team-player-selection :team="$team"/>
 
<div class="mb-4">
<button type="submit"
class="bg-blue-500 text-white px-4 py-2 rounded font-medium">Update Team
</button>
</div>
</form>

That's it! Now our form is reactive, and we can add and remove players as we wish, along with validation:


Submitting the Form Without Livewire

You might have noticed that we didn't submit the form within the Livewire component. This is an intentional decision, as we've built a reactive form that validates the data as we type. This allows us to replace a small part of the form, not the entire one.


You can find the source code for this tutorial on GitHub.