Table of contents

I ran into this issue a few months ago and decided that I should make an article out of it so I can easily find the solution in the future and also help others.

Initial Setup

I’m putting together a web app that uses Laravel and Livewire. Part of this app is having different forms to generate records, and some of the forms are pretty complex with different conditional logic for the fields based on other options selected.

The <select> elements are using Select2 Javascript library to make it more user-friendly and the parent of that <select> is set to use the wire:ignore directive. This makes Select2 run properly inside the component.

<div class="row mb-3" wire:ignore>
    <label for="model_name" class="form-label">Model</label>
    <select class="form-select @error('model_name') is-invalid @enderror" id="model_name" wire:model.lazy="model_name" required>
        <option value="" disabled>Select Model</option>

        @foreach($models as $model)
            <option value="{{ $model['model'] }}">{{ $model['model'] }}</option>
        @endforeach

    </select>

    @error('model_name') 
        <div class="invalid-feedback">
            {{ $message }}
        </div> 
    @enderror
</div>

The Issue

The above example works fine on its own, but as I mentioned the form has conditional logic that comes into play.

The Model will load different values in the select element based on a Brand option that is available at the start of the form. If the user selects a certain Brand, the Models will get populated with the correct values, if it changes again, it will repopulate the values in the select.

This works fine with a normal select, but since we’re using Select2 with wire:ignore on the parent, the options don’t get updated.

<div class="row mb-3">
    <label for="brand_name" class="form-label">Brand</label>

    <div class="dm-custom-radio-wrap dm-custom-radio-wrap-3 @error('brand_name') is-invalid @enderror">
        @foreach($brands as $brand)
            <div class="form-check form-check-inline dm-custom-radio">
                <input type="radio" id="brand_name_{{ $brand->brand }}"  name="brand_name" class="form-check-input" wire:model.lazy="brand_name" value="{{ $brand->brand }}" required>
                <label class="form-check-label" for="brand_name_{{ $brand->brand }}" ><div class="dm-label-circle"><div></div></div>{{ $brand->brand }}</label>
            </div>
        @endforeach
    </div>

    @error('brand_name') 
        <div class="invalid-feedback">
            {{ $message }}
        </div> 
    @enderror
</div>

@if ($brandSelected)
    <div class="row mb-3" wire:ignore>
        <label for="model_name" class="form-label">Model</label>
        <select class="form-select @error('model_name') is-invalid @enderror" id="model_name" wire:model.lazy="model_name" required>
            <option value="" disabled>Select Model</option>

            @foreach($models as $model)
                <option value="{{ $model['model'] }}">{{ $model['model'] }}</option>
            @endforeach

        </select>

        @error('model_name') 
            <div class="invalid-feedback">
                {{ $message }}
            </div> 
        @enderror
    </div>
@endif

Solution

To fix this I wrapped the Brand select HTML into an extra <div> that uses the wire:key directive.

In my Livewire class, I created a public variable that will iterate itself each time the Brand is updating and pass that variable in the wire:key directive, this way forcing the Model that has wire:ignore to refresh with the new values based on the Brand selected.

You also need to emit an event when the Brand is updating to initialize Select2 again for the new <select> that was constructed.

<?php

namespace App\Http\Livewire\Test;

use Livewire\Component;

class Test extends Component
{
    public $iteration = 0;
    public function updatingBrandName()
    {
        $this->emit('brandSelected');
        $this->iteration++;
    }
}

In blade:

<div wire:key="select-field-model-version-{{ $iteration }}"></div>
    <div class="row mb-3" wire:ignore>
        <label for="model_name" class="form-label">Model</label>
        <select class="form-select @error('model_name') is-invalid @enderror" id="model_name" wire:model.lazy="model_name" required>
            <option value="" disabled>Select Model</option>

            @foreach($models as $model)
                <option value="{{ $model['model'] }}">{{ $model['model'] }}</option>
            @endforeach

        </select>

        @error('model_name') 
            <div class="invalid-feedback">
                {{ $message }}
            </div> 
        @enderror
    </div>
</div>

@push('scripts')
    <script>
        Livewire.on('brandSelected', postId => {
            jQuery(document).ready(function () {
                $('#model_name').select2();
                $('#model_name').on('change', function (e) {
                    var data = $('#model_name').select2("val");
                    @this.set('model_name', data);
                });
            });
        })
    </script>
@endpush

Now each time the Brand is changed, the Model will be populated with the correct options even if it’s using wire:ignore and the Select2 will be triggered each time the key is updated.

This method can be applied to any component that uses wire:ignore and you need to update it.

 

Sources