Another tool that we can use to have a dynamic form is VueJS. Our form will look the same but will not have real-time validation (it is possible to do that with VeeValidate, but it is not the scope of this lesson)
To learn the basics of how to get VueJS into your Laravel application, you can read a lesson from one of our courses:
Or check our GitHub commit with all the changes.
Once our setup is complete, we can start working on a component. Let's create a new file, resources/vue/TeamPlayerSelection.vue
, and put the following code there:
<script setup> import {ref} from "vue"; const props = defineProps({ users: { required: true, }, selectedUsers: { required: false }, errorBag: { required: false, default: () => { return {} } }}) const positions = { 'Goalkeeper': 'Goalkeeper', 'Defender': 'Defender', 'Midfielder': 'Midfielder', 'Forward': 'Forward', 'Coach': 'Coach', 'Assistant Coach': 'Assistant Coach', 'Physiotherapist': 'Physiotherapist', 'Doctor': 'Doctor', 'Manager': 'Manager', 'President': 'President',} const players = ref([]) // Setting players from validation bagif (props.selectedUsers.length > 0) { props.selectedUsers.forEach((user) => { players.value.push({ id: user.id ?? "", position: user.position ?? "", }) })} const addUser = () => { players.value.push({ id: "", position: "", })} const removeUser = (index) => { players.value.splice(index, 1)}</script> <template> <div> <div v-for="(user, index) in players" class="mb-4 grid grid-cols-3 gap-4"> <div> <label class="text-xl text-gray-600" :for="'players['+index+'][id]'">Position<span class="text-red-500">*</span></label> <select :name="'players['+index+'][id]'" :id="'players['+index+'][id]'" v-model="user.id" class="border-2 border-gray-300 p-2 w-full"> <option value="">Select Player</option> <option v-for="(name, id) in props.users" :value="id">{{ name }}</option> </select> <div class="text-red-500 mt-2 text-sm mb-4" v-if="errorBag['players.'+index+'.id']"> {{ errorBag['players.'+index+'.id'][0] }} </div> </div> <div> <label class="text-xl text-gray-600" :for="'players['+index+'][position]'">Position<span class="text-red-500">*</span></label> <select :name="'players['+index+'][position]'" :id="'players['+index+'][position]'" v-model="user.position" class="border-2 border-gray-300 p-2 w-full"> <option value="">Select Position</option> <option v-for="(position, key) in positions" :value="key">{{ position }}</option> </select> <div class="text-red-500 mt-2 text-sm mb-4" v-if="errorBag['players.'+index+'.position']"> {{ errorBag['players.'+index+'.position'][0] }} </div> </div> <div> <button @click="removeUser(index)" type="button" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded mt-8"> Remove </button> </div> </div> <div class="text-red-500 mt-2 text-sm mb-4" v-if="errorBag.players"> {{ errorBag.players[0] }} </div> <button @click="addUser" type="button" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mb-4"> Add Player </button> </div></template>
Let's go through the code:
With this code, we are defining the properties our component accepts:
users
- list of users that can be selectedselectedUsers
- list of users that are already selected. This comes either from old()
input or from an already existing teamerrorBag
- Laravel validation error bag to display error messagesconst props = defineProps({ users: { required: true, }, selectedUsers: { required: false }, errorBag: { required: false, default: () => { return {} } }})
Next, we have a positions object (you can have this as a prop as well, but for simplicity, we will keep it here):
const positions = { 'Goalkeeper': 'Goalkeeper', 'Defender': 'Defender', 'Midfielder': 'Midfielder', 'Forward': 'Forward', 'Coach': 'Coach', 'Assistant Coach': 'Assistant Coach', 'Physiotherapist': 'Physiotherapist', 'Doctor': 'Doctor', 'Manager': 'Manager', 'President': 'President',}
Then we initialize an empty players object (that's reactive) to store the selected players. If there are any selected players from the validation bag, we add them to the players' object:
const players = ref([]) // Setting players from validation bagif (props.selectedUsers.length > 0) { props.selectedUsers.forEach((user) => { players.value.push({ id: user.id ?? "", position: user.position ?? "", }) })}
And lastly, we have two methods to add or remove players from the players' object:
const addUser = () => { players.value.push({ id: "", position: "", })} const removeUser = (index) => { players.value.splice(index, 1)}
We have to call the component in our view files to use it.
For create, we need to pass users, old input, and error bag (as we are using Laravel validation) to the component:
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> <team-player-selection :users="{{ json_encode($users) }}" :selected-users="{{ json_encode(old('players', [])) }}" :error-bag="{{ json_encode($errors->getMessageBag()) }}" ></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 for editing, we also need to pass the team players. This is done via old()
default value:
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> <team-player-selection :users="{{ json_encode($users) }}" :selected-users="{{ json_encode(old('players', $teamUsers)) }}" :error-bag="{{ json_encode($errors->getMessageBag()) }}" ></team-player-selection> <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, and now we can create and edit teams with players. It will also display any previous validation issues:
To validate the array, we'll use Laravel Form Request:
app/Http/Requests/StoreTeamRequest.php
public function rules(): array{ return [ 'name' => ['required', 'string', 'max:200'], 'players' => ['required', 'array', 'min:3', 'max:10'], 'players.*.id' => ['required', 'exists:users,id', 'integer', 'distinct'], 'players.*.position' => ['required', 'string', 'max:200', 'distinct'], ];} public function messages(){ return [ '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', ];}
Combining this with native form submit gives us a seamless experience while using VueJS for the dynamic part.
You can find the complete code for this tutorial on GitHub.