Clean Reusable Livewire Modals That You Can Trigger From Anywhere

May 6th, 2021

Everyone loves modals, and with Livewire they're surprisingly easy to implement with the help of Alpine.js. I've been through so many iterations of the perfect modal setup, and *this* is the solution I've arrived at for clean, reusable modals that you can trigger from absolutely anywhere.

This article covers Livewire 2. For the Livewire 3 solution, there's an updated course available here covering everything you need

Starting out

If you're following along, get yourself a fresh Laravel project set up, install Laravel Breeze for some scaffolding, and install Livewire. This is going to make it really easy to focus on the important bits.

Create a base Livewire modal component

Any modal we create needs the ability to show and hide itself. Rather than duplicate that functionality across multiple Livewire components, start with a base component that you can extend.

Create the component.

php artisan livewire:make Modal

Here's what it needs to look like.

namespace App\Http\Livewire;

use Livewire\Component;

class Modal extends Component
{
    public $show = false;

    protected $listeners = [
        'show' => 'show'
    ];

    public function show()
    {
        $this->show = true;
    }
} 

We'll go into more detail on this later, in particular the $listeners we've added here.

Create the modal you want to be shown

This is the modal that's going to show, so the content is up to you. For this example, let's create a contact us modal.

php artisan livewire:make ContactModal

The code for this is super simple, we just extend the base Modal we created in the last step.

namespace App\Http\Livewire;

use Livewire\Component;
use App\Http\Livewire\Modal;

class ContactModal extends Modal
{
    public function render()
    {
        return view('livewire.contact-modal');
    }
}

Create a Blade component for the generic modal

Now we actually need to design a modal. I've done the hard work for you here, but you might want to tweak this for your own needs.

First, create a Blade component for the modal so we can easily re-use it.

php artisan make:component Modal

And add the following markup. I'm using Tailwind to style it.

<div class="fixed inset-0 overflow-y-auto px-4 py-6 md:py-24 sm:px-0 z-40">
    <div class="fixed inset-0 transform">
        <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
    </div>

    <div class="bg-white rounded-lg overflow-hidden transform sm:w-full sm:mx-auto max-w-lg">
        {{ $slot }}
    </div>
</div>

The first child div here is the faded background of the modal (note the opacity-75 class) and the second is the actual content of the modal, added via the default Blade component slot.

Now open up the contact-modal.blade.php Livewire view and add the following.

<div>
    <x-modal>
        <div class="p-6">
            Contact modal
        </div>
    </x-modal>
</div>

Add the modal somewhere

If you want to a modal to be global to your entire site, you can add it to your app.blade.php base template. Or, if it belongs in one place only, add it there.

I'm adding it to the bottom of the app.blade.php file.

            {{-- The rest of your app.blade.php file up here --}}
            
            <livewire:scripts />
    
            <livewire:contact-modal />
        </body>
    </html>

If you refresh now, you should see your modal displayed. Obviously not ideal to have it always display. So, we need to...

Add the display hook

Open contact-modal.blade.php and update it to pass through a Livewire attribute.

<div>
    <x-modal wire:model="show"> {{-- <-- Add this --}}
        <div class="p-6">
            Contact modal
        </div>
    </x-modal>
</div>

Now update the modal.blade.php Blade component to take this value and only show the modal if show is true.

If you're confused at this point, we'll run through this shortly.

<div
    x-data="{
        show: @entangle($attributes->wire('model')).defer
    }"
    x-show="show"
    x-on:keydown.escape.window="show = false"
    class="fixed inset-0 overflow-y-auto px-4 py-6 md:py-24 sm:px-0 z-40"
>
    <div x-show="show" class="fixed inset-0 transform" x-on:click="show = false">
        <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
    </div>

    <div x-show="show" class="bg-white rounded-lg overflow-hidden transform sm:w-full sm:mx-auto max-w-lg">
        {{ $slot }}
    </div>
</div>

Ok, let's break down what's happening.

  1. Our base Modal component has a show property (and thus so does our contact modal, because it extends it).
  2. From the contact modal, we pass in wire:model="show" to tell the Blade component the true/false value of this.
  3. In the modal Blade component, we use Alpine.js to set a property using @entangle. This shares the show property value from the Livewire component with Alpine.js and keeps it in sync.
  4. We add some x-show conditions to parts of the modal so they're only shown if the show property is true.
  5. We add a x-on:keydown.escape.window event handler to set show to false if the escape key is pressed.
  6. We add a x-on:click event handler to set show to false if the faded background of the modal is clicked (anywhere outside the actual modal content).

If you refresh right now, you'll no longer see the modal. Try setting the $show property on the Modal.php Livewire component to true and refreshing again. You should see your modal appear! Don't forget to return it back to false.

Triggering the modal

Now, like magic, from anywhere in your app, you can show this modal. Here's an example of a button you could press to trigger the contact modal.

<button x-data="{}" x-on:click="window.livewire.emitTo('contact-modal', 'show')" class="text-indigo-500">
    Show contact modal
</button>

This emits a show event to the contact modal.

Remember the $listeners from the base Modal Livewire component? This picks up on the event and sets the $show property to true. Because we used @entangle, this keeps in sync with Alpine.js and shows the modal.

You can even trigger the modal directly from another Livewire component.

$this->emitTo('contact-modal', 'show');

Reuse the functionality

The beauty of this solution is that it's quickly reusable. You'll just need to:

  1. Create another Livewire component for your new modal, making sure you extend the base Modal component.
  2. Fill in the Livewire view with the Blade modal component.
  3. Trigger it using the component name.

We're done

Although the solution is simple, if you're new to Livewire or Alpine.js this might be a lot to take in. If you'd like to learn more, we have loads of courses that'll get you up to speed.

Happy modaling!

Thanks for reading! If you found this article helpful, you might enjoy our practical screencasts too.
Author
Alex Garrett-Smith
Share :

Comments

No coments, yet. Be the first to leave a comment.