JeremyWebilo's avatar

Livewire - Modals and form binding

Hello ! I've some problems with Livewire, I'm new to deal with it and I've created a project for learn and practice Livewire.

I've created a Livewire component "ManageSites", I want to make a CRUD with my Site model inside this component, I've created table and I use some modals for create, edit and delete. In the create / edit modal, I've added a form (input text and input checkbox), here all going succeed, but when I change an input (checkbox checked to checkbox unchecked) my modal close automatically, it's not my intention and I don't understand what going on...

Here a video for my problem: https://streamable.com/l3mb4j

ManageSite.php

<?php

namespace App\Http\Livewire;

use App\Models\Site;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Livewire\Component;

class ManageSites extends Component
{
    public bool $showCreateModal, $showConfirmDeleteModal = false;
    public $state = [];
    public bool $updateMode = false;

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

    public function store()
    {
        Validator::make($this->state, [
            'domain' => 'required',
            'is_active' => 'nullable',
        ])->validate();

        Auth::user()->sites()->save(new Site([
            'domain' => $this->state['domain'],
            'is_active' => isset($this->state['is_active']) && $this->state['is_active'] ? 1 : 0,
        ]));

        $this->reset('state');
        $this->showCreateModal = false;
    }

    public function openEditModal(int $id)
    {
        $this->updateMode = true;

        $site = Auth::user()->sites()->findOrFail($id);
        $this->state = $site->toArray();

        $this->showCreateModal = true;
    }

    public function update()
    {
        Validator::make($this->state, [
            'is_active' => 'nullable',
        ])->validate();

        $site = Auth::user()->sites()->findOrFail($this->state['id']);
        $site->update([
            'is_active' => isset($this->state['is_active']) && $this->state['is_active'] ? 1 : 0,
        ]);

        $this->showEditModal = false;
        $this->reset('state');
        $this->updateMode = false;
    }

    public function confirmDelete(int $id)
    {
        $site = Auth::user()->sites()->findOrFail($id);
        $this->state = $site->toArray();

        $this->showConfirmDeleteModal = true;
    }

    public function delete()
    {
        $site = Auth::user()->sites()->findOrFail($this->state['id']);
        $site->delete();

        $this->showConfirmDeleteModal = false;
    }

    public function render()
    {
        return view('livewire.manage-sites')
            ->with('sites', Auth::user()->sites()->get());
    }
}

manage-site.blade.php

<div>
    <x-utils.button class="bg-blue-400 hover:bg-blue-500" wire:click="$set('showCreateModal', true)">
        Créer un site
    </x-utils.button>
    <table>
        <thead>
        <tr>
            <th>Domain</th>
            <th>Active</th>
            <th>Created at</th>
            <th>Actions</th>
        </tr>
        </thead>
        <tbody>
        @foreach($sites as $site)
            <tr>
                <td>{{ $site->domain }}</td>
                <td>{{ $site->is_active ? 'Oui' : 'Non' }}</td>
                <td>{{ $site->created_at }}</td>
                <td>
                    <x-utils.button class="bg-gray-400 hover:bg-gray-500" wire:click="openEditModal({{ $site->id }})">
                        Edit
                    </x-utils.button>
                    <x-utils.button class="bg-red-400 hover:bg-red-500" wire:click="confirmDelete({{ $site->id }})">
                        Delete
                    </x-utils.button>
                </td>
            </tr>
        @endforeach
        </tbody>
    </table>

    <!-- START - Create Modal -->
    <x-utils.modal wire:model="showCreateModal">
        <x-slot name="header">
            @if($updateMode)
                <h1>Edit</h1>
            @else
                <h1>Create</h1>
            @endif
        </x-slot>

        <x-slot name="main">
            <form>
                @if(!$updateMode)
                    <label for="domain">Domain</label>
                    <input id="domain" type="text" wire:model="state.domain">
                @endif
                <label for="active">Active</label>
                <input id="active" type="checkbox" wire:model="state.is_active">
            </form>
        </x-slot>

        <x-slot name="footer">
            @if($updateMode)
                <x-utils.button class="bg-blue-400 hover:bg-blue-500" wire:click="update">Edit</x-utils.button>
            @else
                <x-utils.button class="bg-blue-400 hover:bg-blue-500" wire:click="store">Create</x-utils.button>
            @endif
        </x-slot>
    </x-utils.modal>
    <!-- END - Create Modal -->

    <!-- START - Delete Modal -->
    <x-utils.modal wire:model="showConfirmDeleteModal">
        <x-slot name="header">
            <h1>Delete</h1>
        </x-slot>

        <x-slot name="main">
            <p>Are you sure ?</p>
        </x-slot>

        <x-slot name="footer">
            <x-utils.button class="bg-gray-400 hover:bg-gray-500" wire:click="$set('showConfirmDeleteModal', false)">
                Cancel
            </x-utils.button>
            <x-utils.button class="bg-red-400 hover:bg-red-500" wire:click="delete">Delete</x-utils.button>
        </x-slot>
    </x-utils.modal>
    <!-- END - Delete Modal -->
</div>

And my modal component

<div {{ $attributes }}
     x-data="{ show : @entangle($attributes->wire('model')) }"
     x-show="show"
     style="display: none"
>
    <div class="fixed inset-0 bg-gray-900 opacity-90" x-on:click="show = false"></div>

    <div class="bg-white shadow-md p-4 max-w-sm h-48 m-auto rounded-md fixed inset-0">
        <div class="flex flex-col h-full justify-between">
            <header class="font-bold text-lg">
                {{ $header }}
            </header>

            <main class="mb-4">
                {{ $main }}
            </main>

            <footer>
                {{ $footer }}
            </footer>
        </div>
    </div>
</div>

0 likes
12 replies
vincent15000's avatar

This happens because when you check / uncheck the checkbox, Livewire automatically refreshes the view.

To avoid the modal to close when you check / uncheck the checkbox, you can for example add wire:ignore or wire:ignore.self to the modal wrapper.

https://laravel-livewire.com/docs/2.x/reference#template-directives

I don't know if this template directive is available with Livewire version 3, you will perhaps need to read the documentation to find an equivalent.

JeremyWebilo's avatar

@vincent15000 Thanks for your reply I didn't know wire:ignore, I tried to put it on the modal wrapper but nothing changes. Maybe my structure is wrong...

An example:

<!-- START - Create Modal -->
    <x-utils.modal wire:model="showCreateModal" wire:ignore>
        <x-slot name="header">
            @if($updateMode)
                <h1>Edit</h1>
            @else
                <h1>Create</h1>
            @endif
        </x-slot>

        <x-slot name="main">
            <form>
                @if(!$updateMode)
                    <label for="domain">Domain</label>
                    <input id="domain" type="text" wire:model="state.domain">
                @endif
                <div>
                    <label for="active">Active</label>
                    <input id="active" type="checkbox" wire:model="state.is_active">
                </div>
            </form>
        </x-slot>

        <x-slot name="footer">
            @if($updateMode)
                <x-utils.button class="bg-blue-400 hover:bg-blue-500" wire:click="update">Edit</x-utils.button>
            @else
                <x-utils.button class="bg-blue-400 hover:bg-blue-500" wire:click="store">Create</x-utils.button>
            @endif
        </x-slot>
    </x-utils.modal>
    <!-- END - Create Modal -->
1 like
JeremyWebilo's avatar

@vincent15000 Yes...

<div {{ $attributes }}
     x-data="{ show : @entangle($attributes->wire('model')) }"
     x-show="show"
     style="display: none"
>
    <div class="fixed inset-0 bg-gray-900 opacity-90" x-on:click="show = false"></div>

    <div class="bg-white shadow-md p-4 max-w-sm h-48 m-auto rounded-md fixed inset-0">
        <div class="flex flex-col h-full justify-between">
            <header class="font-bold text-lg">
                {{ $header }}
            </header>

            <main class="mb-4">
                {{ $main }}
            </main>

            <footer>
                {{ $footer }}
            </footer>
        </div>
    </div>
</div>
1 like
vincent15000's avatar

@JeremyWebilo Oh yes effectively, if you check / uncheck the checkbox which is inside the modal, necessarily it will refresh the modal, so wire:ignore isn't useful here.

Can you show the controller for the modal please ?

JeremyWebilo's avatar

@vincent15000 The livewire component you meant ?

<?php

namespace App\Http\Livewire;

use App\Models\Site;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Livewire\Component;

class ManageSites extends Component
{
    public bool $showCreateModal, $showConfirmDeleteModal = false;
    public $state = [];
    public bool $updateMode = false;

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

    public function store()
    {
        Validator::make($this->state, [
            'domain' => 'required',
            'is_active' => 'nullable',
        ])->validate();

        Auth::user()->sites()->save(new Site([
            'domain' => $this->state['domain'],
            'is_active' => isset($this->state['is_active']) && $this->state['is_active'] ? 1 : 0,
        ]));

        $this->reset('state');
        $this->showCreateModal = false;
    }

    public function openEditModal(int $id)
    {
        $this->updateMode = true;

        $site = Auth::user()->sites()->findOrFail($id);
        $this->state = $site->toArray();

        $this->showCreateModal = true;
    }

    public function update()
    {
        Validator::make($this->state, [
            'is_active' => 'nullable',
        ])->validate();

        $site = Auth::user()->sites()->findOrFail($this->state['id']);
        $site->update([
            'is_active' => isset($this->state['is_active']) && $this->state['is_active'] ? 1 : 0,
        ]);

        $this->showEditModal = false;
        $this->reset('state');
        $this->updateMode = false;
    }

    public function confirmDelete(int $id)
    {
        $site = Auth::user()->sites()->findOrFail($id);
        $this->state = $site->toArray();

        $this->showConfirmDeleteModal = true;
    }

    public function delete()
    {
        $site = Auth::user()->sites()->findOrFail($this->state['id']);
        $site->delete();

        $this->showConfirmDeleteModal = false;
    }

    public function render()
    {
        return view('livewire.manage-sites')
            ->with('sites', Auth::user()->sites()->get());
    }
}

1 like
vincent15000's avatar

@JeremyWebilo Would it happen because you specify style="display: none" in the modal div ?

I would remove this style : first it's not a good pratice to have a style attribute in a tag, second it could be the problem, third I wonder if it has any effect.

JeremyWebilo's avatar

@vincent15000 Nothing change when I remove style="display: none" . I don't think my structure is good, so I rebuild it, thanks for your help !

1 like
webrobert's avatar

Thanks @vincent15000

@jeremywebilo, for the original code [on first pass]. I suspect this is a Dom diff issue. Like you've missed a close tag somewhere. or a div wrap on your conditions.

Like here...

 @if(!$updateMode)
	<label for="domain">Domain</label>
 	<input id="domain" type="text" wire:model="state.domain">
 @endif

Instead I think I would do

 @if(!$updateMode)
<div>
	<label for="domain">Domain</label>
    <input id="domain" type="text" wire:model="state.domain">
</div>
@endif

So the rerender misses something.

Have you read the troubleshooting section of the livewire docs?

https://laravel-livewire.com/docs/2.x/troubleshooting

1 like

Please or to participate in this conversation.