Back to Course |
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/select2@4.1.0-rc.0/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/select2@4.1.0-rc.0/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