Looking for tips working with modals

Scenario;

  • Single Livewire component.
  • Multi-row table, each row with a clickable item for ‘further details’
  • On clicking the button, a modal opens with the relevant details for that row, including a reasonable sized image.
  • Want to avoid having multiple modals (one per row) so need to insert the selected content into the common modal.

Approach #1. Modal is a livewire component that opens itself and closes itself when asked. Drawback, laggy mouseclicks whilst the request to open/close does the roundtrip. Benefit, when modal opens, it has the right content.

Approach #2. Modal is opened and closed with Alpine. emits an event to Livewire component telling it which content to load into the modal. Drawback, modal shows previous content until new content arrives from Livewire. Advantage, open and close is snappy because it is dealt with locally.

I’m considering the best approach. Ideally #2, but what I would like is to replace the content of the modal with a loading spinner when it is closed, so that when opened the spinner shows not the previous content. Ideally this would be done in-line in the modal and not by calling to separate script which would make packaging as a component more difficult.

Possibly also try and hook in prefetch.

Anyone done this that can share some tips?

2 Likes

I use method 2.

I have a modal component that has nested components for the different modals I use and trigger it with a combo of Alpine and livewire events. By default, it’s has a loading indicator that uses wire:loading to display.

Assuming you are using Bootstrap modal, here’s an Approach #1 - kinda - working example that I find to make Bootstrap play nicely with Livewire.
Maybe not 100% correct but it’s working for me so far.

From the Index Component take advantage of Livewire Events to tell the Form Component what entity it shoud load and from the Form Component also emit an event to toggle the Bootstrap modal in the Javascript side.

routes/web.php

Route::livewire('/galaxies', 'galaxy.index')->name('galaxies.index');

Http/Livewire/Galaxy/Index.php

<?php

namespace App\Http\Livewire\Galaxy;

use Livewire\Component;

class Index extends Component
{
    public function render()
    {
        return view('livewire.galaxy.index', [
            'galaxies' => [
                1 => 'Milky Way',
                2 => 'Andromeda',
                3 => 'Triangulum',
            ]
        ]);
    }
}

Http/Livewire/Galaxy/Form.php

<?php

namespace App\Http\Livewire\Galaxy;

use Livewire\Component;

class Form extends Component
{
    public $name;

    protected $listeners = ['open' => 'loadGalaxy'];

    public function loadGalaxy($uid)
    {
        // $this->resetErrorBag();
        // $this->fill($uid ? Galaxy::findOrFail($uid) : new Galaxy);
        
        $galaxies = collect([1 => 'Milky Way', 2 => 'Andromeda', 3 => 'Triangulum']);
        $this->name = $galaxies->get($uid) ?? '';

        $this->emit('toggleGalaxyFormModal');
    }

    public function submit()
    {
        // @todo Validate and Save

        $this->emit('toggleGalaxyFormModal');
    }

    public function render()
    {
        return view('livewire.galaxy.form');
    }
}

resources/views/livewire/galaxy/index.blade.php

<div>
    <div wire:ignore>
        <div class="modal fade" id="galaxy-form-modal" tabindex="-1" role="dialog" aria-hidden="true">
            <livewire:galaxy.form>
        </div>
    </div>

    <button type="button" wire:click="$emitTo('galaxy.form', 'open')">Create Galaxy</button>

    @foreach($galaxies as $id => $galaxy)
        <div wire:key="{{ $id }}">
            <a href wire:click.prevent="$emitTo('galaxy.form', 'open', {{ $id }})">
                {{ $galaxy }}
            </a>
        </div>
    @endforeach
</div>

resources/views/livewire/galaxy/form.blade.php

<div class="modal-dialog modal-xl">
    <div class="modal-content">
        <div class="modal-header">
            <h5 class="modal-title">Galaxy Form</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        </div>
        <form autocomplete="off">
            <div class="modal-body">
                <input type="text" wire:model="name">
            </div>
            <div class="modal-footer">
                <button type="button" wire:click="submit">Submit</button>
            </div>
        </form>
    </div>
</div>

resources/js/app.js

window.livewire.on('toggleGalaxyFormModal', () => $('#galaxy-form-modal').modal('toggle'));
2 Likes

wire:loading helps a little but as the bulk of the modal is an image, the Livewire loading state finishes with the swapping of the image src tag for the new image src. During the loading of the image by the browser, the old image is still shown and then is replaced once the download completes. I think the solution is probably going to require removing the image from the dom, or replacing it with a spinner or simple black rectangle whilst sending the livewire event to the server.

Demo here shown with 3G network restriction

modal

I use some javascript to emit an event and reset the contents of the modal:

        $('#highlightModal').on('hidden.bs.modal', function () {
            @this.call('resetContents');
        });

I added a line to empty the div containing the image (this is the code behind the eye icon)

    <a href="#" x-on:click.prevent="
        open = true;
        window.livewire.emit('setcontent',{{ $answer->question->id ?? 0 }})
        document.getElementById('modal-image').innerHTML='';
    ">

This prevents the old image from showing.

I don’t particularly like it because it needs to refer to an id of an element inside the Livewire component’s view, and therefore feels fragile. If I changed that div or the internal structure of the Livewire component then this would break.

(ps. NOT bootstrap modal)

Hi Snapey,

You could take a look into progressive image loading (css and/or javascript) techniques.

This article can be useful to give you some ideas https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Loading

(Sorry for my previous reply, I didn’t know you already have a working scenario with modal. I thought you were looking for building a listing + modal from scratch)

Can you show a quick example for method 2? How do you dynamically load a livewire component into the page?

1 Like

In the view, on the button;

    <a href="#" x-on:click.prevent="
        open = true;
        window.livewire.emit('setcontent',{{ $answer->question->id ?? 0 }})
        document.getElementById('modal-image').innerHTML='';
    ">

open=true is telling Alpine to open the modal window

window.livewire.emit sends a ‘setcontent’ event to any livewire component that is listening and passes it the id of the content to render

the final statement clears the previous content

Then in the Livewire component which is within the modal, a listener is created for the ‘setcontent’ event and it renders the view for the required question.

1 Like

What if I need some data from variables in that modal and those data are based on the variable ($id) passed to the form component. I am stuck in that senario and getting this error:

ErrorException
Trying to get property ‘flex_start_date’ of non-object

Is there any solution to tackle this senario and load the data based of the value of $id variable.