Courses

Creating a Quiz System with Laravel 10 + Livewire 3: Step-by-Step

Admin: Add Questions to Quiz with Select2

In this part, we will create a Blade component for Select2 and will use it for selecting questions for a quiz.


So, first let's create the component and add scripts.

php artisan make:component SelectList

With this component, we also create a class that will accept the $options parameter.

app/View/Components/SelectList.php:

class SelectList extends Component
{
/**
* Create a new component instance.
*/
public function __construct(public mixed $options, public mixed $selectedOptions)
{
//
}
 
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.select-list');
}
}

And the blade component:

resources/views/components/select-list.blade.php:

<div>
<div wire:ignore class="w-full">
<select class="select2 w-full" data-placeholder="Select your option" {{ $attributes }}>
@if(!isset($attributes['multiple']))
<option></option>
@endif
@foreach($options as $key => $value)
<option value="{{ $key }}" @selected(in_array($key, $selectedOptions))>{{ $value }}</option>
@endforeach
</select>
</div>
</div>
 
@push('scripts')
<script>
document.addEventListener('livewire:init', () => {
let el = $('#{{ $attributes['id'] }}')
 
function initSelect() {
el.select2({
placeholder: 'Select your option',
allowClear: !el.attr('required')
})
}
 
initSelect()
Livewire.hook('message.processed', (message, component) => {
initSelect()
});
el.on('change', function (e) {
let data = $(this).select2("val")
if (data === "") {
data = null
}
@this.set('{{ $attributes['wire:model'] }}', data)
});
});
</script>
@endpush

But before using this component we need to set up Select2 using CDN. Also, in the blade component we are using the @push blade directive to render JS code in the layout file. For this, to work we need to add @stack('scripts') into the main layout file.

resources/views/layouts/app.blade.php:

// ...
<!-- Scripts -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" />
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
 
// ...
 
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
@livewireScripts
@stack('scripts')
</body>
</html>

Now we need to load all the questions in the QuizForm Livewire component.

app/Livewire/Quiz/QuizForm.php:

use App\Models\Question;
 
class QuizForm extends Component
{
public ?Quiz $quiz = null;
 
public string $title = '';
public string $slug = '';
public string|null $description = '';
public bool $published = false;
public bool $public = false;
 
public bool $editing = false;
 
public array $listsForFields = [];
 
public function mount(Quiz $quiz): void
{
$this->initListsForFields();
 
if ($quiz->exists) {
$this->quiz = $quiz;
$this->editing = true;
$this->title = $quiz->title;
$this->slug = $quiz->slug;
$this->description = $quiz->description;
$this->published = $quiz->published;
$this->public = $quiz->public;
} else {
$this->published = false;
$this->public = false;
}
}
 
// ...
 
protected function initListsForFields(): void
{
$this->listsForFields['questions'] = Question::pluck('question_text', 'id')->toArray();
}
}

In the form, we will add this field under the description.

resources/views/livewire/quiz/quiz-form.blade.php:

// ...
<div class="mt-4">
<x-input-label for="questions" value="Questions" />
<x-select-list class="w-full" id="questions" name="questions" :options="$this->listsForFields['questions']" :selectedOptions="$questions" wire:model="questions" multiple />
<x-input-error :messages="$errors->get('questions')" class="mt-2" />
</div>
// ...

Now in the create form after selecting questions input you should see the all questions list.

questions select2 input

As you can see we bind this question's input into the questions property. So let's add that to the QuizForm component.

app/Livewire/Quiz/QuizForm.php:

class QuizForm extends Component
{
public ?Quiz $quiz = null;
 
public string $title = '';
public string $slug = '';
public string|null $description = '';
public bool $published = false;
public bool $public = false;
 
public bool $editing = false;
 
public array $questions = [];
 
public array $listsForFields;
// ...
}

Next, before saving we need to create a Many to Many relationship for the Quiz model with the Question.

php artisan make:migration "create question quiz table"

database/migrations/xxxx_create_question_quiz_table.php:

return new class extends Migration
{
public function up(): void
{
Schema::create('question_quiz', function (Blueprint $table) {
$table->foreignId('question_id')->constrained()->cascadeOnDelete();
$table->foreignId('quiz_id')->constrained()->cascadeOnDelete();
});
}
};

app/Models/Quiz.php:

class Quiz extends Model
{
// ...
public function questions(): BelongsToMany
{
return $this->belongsToMany(Question::class);
}
}

app/Models/Question.php:

class Question extends Model
{
// ...
public function quizzes(): BelongsToMany
{
return $this->belongsToMany(Quiz::class);
}
}

Now we can sync Quiz with the Questions.

app/Livewire/Quiz/QuizForm.php:

class QuizForm extends Component
{
// ...
 
public function save(): Redirector|RedirectResponse
{
$this->validate();
 
if (empty($this->quiz)) {
$this->quiz = Quiz::create($this->only(['title', 'slug', 'description', 'published', 'public']));
} else {
$this->quiz->update($this->only(['title', 'slug', 'description', 'published', 'public']));
}
 
$this->quiz->questions()->sync($this->questions);
 
return to_route('quizzes');
}
 
// ...
}

The only thing we have left to do in the form is to load questions when editing the quiz.

app/Livewire/Quiz/QuizForm.php:

class QuizForm extends Component
{
// ...
public function mount(Quiz $quiz): void
{
$this->initListsForFields();
 
if ($quiz->exists) {
$this->quiz = $quiz;
$this->editing = true;
$this->title = $quiz->title;
$this->slug = $quiz->slug;
$this->description = $quiz->description;
$this->published = $quiz->published;
$this->public = $quiz->public;
 
$this->questions = $quiz->questions()->pluck('id')->toArray();
} else {
$this->published = false;
$this->public = false;
}
}
// ...
}

edit quiz form

If you want to have a style for the select2 as it is in the screen above, add below CSS code to the resources/css/app.css and compile assets.

/* Select 2 */
.select2 {
@apply w-full border-0 placeholder-gray-300 text-gray-600 bg-white rounded text-sm shadow focus:outline-none focus:ring !important;
}
 
.select2-dropdown {
@apply absolute block w-auto box-border bg-white shadow-lg border-blue-200 z-50 float-left;
}
 
.select2-container--default .select2-selection--single {
@apply border-0 h-11 flex items-center text-sm
}
 
.select2-container--default .select2-selection--multiple {
@apply border-0 text-sm mr-1
}
 
.select2-container--default.select2-container--focus .select2-selection--single,
.select2-container--default.select2-container--focus .select2-selection--multiple {
@apply border-0 outline-none ring ring-indigo-500
}
 
.select2-container--default .select2-selection--single .select2-selection__arrow {
top: 9px;
}
 
.select2-container .select2-selection--single .select2-selection__rendered {
@apply px-3 py-3 text-blue-600
}
 
.select2-container .select2-selection--multiple .select2-selection__rendered {
@apply px-3 py-2 text-blue-600
}
 
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: inherit;
}
 
.select2-selection__choice {
@apply text-xs font-semibold inline-block py-1 px-2 rounded bg-gray-800 border-0 !important;
}
 
.select2-selection__choice span {
@apply text-white !important;
}
 
.select2-search__field:focus {
outline: none;
}
 
.select2-container--default .select2-selection--single .select2-selection__clear {
@apply text-rose-500 ml-1 !important
}
 
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
@apply text-gray-300 mr-1 static !important
}
 
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
@apply bg-gray-600 !important
}
 
.select-all, .deselect-all {
@apply cursor-pointer;
font-size: 0.5rem !important;
}

One last thing in this lesson. Let's show the count of questions in the quizzes list. I will add it after the description.

resources/views/livewire/quiz/quiz-list.blade.php:

// ...
<th class="bg-gray-50 px-6 py-3 text-left">
<span class="text-xs font-medium uppercase leading-4 tracking-wider text-gray-500">Description</span>
</th>
<th class="bg-gray-50 px-6 py-3 text-left"> {{-- [tl! add:start --}}
<span class="text-xs font-medium uppercase leading-4 tracking-wider text-gray-500">Questions count</span>
</th> {{-- [tl! add:end --}}
 
// ...
 
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
{{ $quiz->description }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap"> {{-- [tl! add:start --}}
{{ $quiz->questions_count }}
</td> {{-- [tl! add:end --}}
// ...

Don't forget to eager load so that you won't get an N+1 problem!

app/Livewire/Quiz/QuizList.php:

class QuizList extends Component
{
public function render(): View
{
$quizzes = Quiz::latest()->paginate();
$quizzes = Quiz::withCount('questions')->latest()->paginate();
 
return view('livewire.quiz.quiz-list', compact('quizzes'));
}
// ...
}

questions count

Previous: Admin: Quizzes CRUD
avatar

Hello I am having an erro "trim(): Argument #1 ($string) must be of type string, array given"

select-list-blade file

quiz-form.blade file

what can i do to fix this error?

avatar

select-list-blade file - https://prnt.sc/oSt058CovRWc

quiz-form.blade file - https://prnt.sc/GcAHvqV9pN19

avatar

@PedroSMaia it's a very wierd error that only some people get. How does your app/View/Components/SelectList.php look? But adding props options to the component should fix error.

avatar

@Nerijus my app/View/Components/SelectList.php print is on the link bellow https://prnt.sc/t9u0HSGJP8-4

avatar

Sorry wrong file, I meant app/Http/View/Components/SelectList.php. But have you tried adding props for options to the blade file?

avatar

Hi again, my file app/Http/View/Components/SelectList.php is in the printscreen bellow https://prnt.sc/FGPNaXeOQyJt

i don't understand what do you say about props

avatar

Your SelectList.php is different from the in this tutorial.

avatar

@PedroSMaia, you are missing parameters inside construct method of Selectlist.php

public function __construct(public mixed $options)

avatar

i have the same error as @PedroSMaia, i added this inside my SelectList.php:

public function __construct(public mixed $options)

and now a different error occurs:

"Unresolvable dependency resolving Parameter #0 mixed $options in class App\View\Components\SelectList"

help anyone?

avatar

i just solved my problem. i ran the following command:

php artisan optimize:clear

seems like its a caching issue

avatar
You can use Markdown
avatar
Luis Antonio Parrado

If you are working with Livewire 3 must replace the event livewire:load by livewire:init

avatar
You can use Markdown
avatar
You can use Markdown