Add confirm delete modal to filament v2 repeater items

954 views Asked by At

I have a correctly working FilamentPHP v2 repeater. I am trying to add a 'confirm delete' modal that the user must accept before the delete action proceeds, a pretty normal use case.

There doesn't seem to be anything built in to handle this, and the views are generated - so firing my own events from the delete click in the component seems like it will be tricky.

I have also tried using the built-in events, such as repeater:deleteItem - but this fires AFTER the delete has taken place, which is too late as it has already been removed from the interface.

Is there a standard way to deal with this that I've missed?

1

There are 1 answers

2
AmooAti On

You can add the confimation dialog as document said.

Here's an example:

Forms\Components\Repeater::make('test')
    ->schema([
        Forms\Components\TextInput::make('namee')->required()
    ])
    ->deleteAction(fn(Forms\Components\Actions\Action $action) => $action->requiresConfirmation())

Updated (V2): Since the previous solution doesn't work in version 2, Below two options are available:

First Option: You can override the repeater component to add a confirmation. To do this, you need to publish filament form views using php artisan vendor:publish --provider="Filament\Forms\FormsServiceProvider" and then customize the repeater component located in resources/views/vendors/forms/components/repeater.blade.php and add onclick event to the trash icon.

Here's an example of final code:


<x-dynamic-component
    :component="$getFieldWrapperView()"
    :id="$getId()"
    :label="$getLabel()"
    :label-sr-only="$isLabelHidden()"
    :helper-text="$getHelperText()"
    :hint="$getHint()"
    :hint-action="$getHintAction()"
    :hint-color="$getHintColor()"
    :hint-icon="$getHintIcon()"
    :required="$isRequired()"
    :state-path="$getStatePath()"
>
    @php
        $containers = $getChildComponentContainers();

        $isCollapsible = $isCollapsible();
        $isCloneable = $isCloneable();
        $isReorderableWithButtons = $isReorderableWithButtons();
        $isItemCreationDisabled = $isItemCreationDisabled();
        $isItemDeletionDisabled = $isItemDeletionDisabled();
        $isItemMovementDisabled = $isItemMovementDisabled();
        $hasItemLabels = $hasItemLabels();
    @endphp

    <div>
        @if ((count($containers) > 1) && $isCollapsible)
            <div class="space-x-2 rtl:space-x-reverse" x-data="{}">
                <x-forms::link
                    x-on:click="$dispatch('repeater-collapse', '{{ $getStatePath() }}')"
                    tag="button"
                    size="sm"
                >
                    {{ __('forms::components.repeater.buttons.collapse_all.label') }}
                </x-forms::link>

                <x-forms::link
                    x-on:click="$dispatch('repeater-expand', '{{ $getStatePath() }}')"
                    tag="button"
                    size="sm"
                >
                    {{ __('forms::components.repeater.buttons.expand_all.label') }}
                </x-forms::link>
            </div>
        @endif
    </div>

    <div
        {{
            $attributes
                ->merge($getExtraAttributes())
                ->class([
                    'filament-forms-repeater-component space-y-6 rounded-xl',
                    'bg-gray-50 p-6' => $isInset(),
                    'dark:bg-gray-500/10' => $isInset() && config('forms.dark_mode'),
                ])
        }}
    >
        @if (count($containers))
            <ul>
                <x-filament-support::grid
                    :default="$getGridColumns('default')"
                    :sm="$getGridColumns('sm')"
                    :md="$getGridColumns('md')"
                    :lg="$getGridColumns('lg')"
                    :xl="$getGridColumns('xl')"
                    :two-xl="$getGridColumns('2xl')"
                    wire:sortable
                    wire:end.stop="dispatchFormEvent('repeater::moveItems', '{{ $getStatePath() }}', $event.target.sortable.toArray())"
                    class="gap-6"
                >
                    @foreach ($containers as $uuid => $item)
                        <li
                            x-data="{
                                isCollapsed: @js($isCollapsed($item)),
                            }"
                            x-on:repeater-collapse.window="$event.detail === '{{ $getStatePath() }}' && (isCollapsed = true)"
                            x-on:repeater-expand.window="$event.detail === '{{ $getStatePath() }}' && (isCollapsed = false)"
                            wire:key="{{ $this->id }}.{{ $item->getStatePath() }}.{{ $field::class }}.item"
                            wire:sortable.item="{{ $uuid }}"
                            x-on:expand-concealing-component.window="
                                error = $el.querySelector('[data-validation-error]')

                                if (! error) {
                                    return
                                }

                                isCollapsed = false

                                if (document.body.querySelector('[data-validation-error]') !== error) {
                                    return
                                }

                                setTimeout(
                                    () =>
                                        $el.scrollIntoView({
                                            behavior: 'smooth',
                                            block: 'start',
                                            inline: 'start',
                                        }),
                                    200,
                                )
                            "
                            @class([
                                'filament-forms-repeater-component-item relative rounded-xl border border-gray-300 bg-white shadow-sm',
                                'dark:border-gray-600 dark:bg-gray-800' => config('forms.dark_mode'),
                            ])
                        >
                            @if ((! $isItemMovementDisabled) || (! $isItemDeletionDisabled) || $isCloneable || $isCollapsible || $hasItemLabels)
                                <header
                                    @if ($isCollapsible) x-on:click.stop="isCollapsed = ! isCollapsed" @endif
                                    @class([
                                        'flex h-10 items-center overflow-hidden rounded-t-xl border-b bg-gray-50',
                                        'dark:border-gray-700 dark:bg-gray-800' => config('forms.dark_mode'),
                                        'cursor-pointer' => $isCollapsible,
                                    ])
                                >
                                    @unless ($isItemMovementDisabled)
                                        <button
                                            title="{{ __('forms::components.repeater.buttons.move_item.label') }}"
                                            x-on:click.stop
                                            wire:sortable.handle
                                            wire:keydown.prevent.arrow-up="dispatchFormEvent('repeater::moveItemUp', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                            wire:keydown.prevent.arrow-down="dispatchFormEvent('repeater::moveItemDown', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                            type="button"
                                            @class([
                                                'flex h-10 w-10 flex-none items-center justify-center border-r text-gray-400 outline-none transition hover:text-gray-500 focus:bg-gray-500/5',
                                                'dark:border-gray-700 dark:focus:bg-gray-600/20' => config('forms.dark_mode'),
                                            ])
                                        >
                                            <span class="sr-only">
                                                {{ __('forms::components.repeater.buttons.move_item.label') }}
                                            </span>

                                            <x-heroicon-s-switch-vertical
                                                class="h-4 w-4"
                                            />
                                        </button>
                                    @endunless

                                    <p
                                        @class([
                                            'flex-none truncate px-4 text-xs font-medium text-gray-600',
                                            'dark:text-gray-400' => config('forms.dark_mode'),
                                        ])
                                    >
                                        {{ $getItemLabel($uuid) }}
                                    </p>

                                    <div class="flex-1"></div>

                                    <ul
                                        @class([
                                            'flex divide-x rtl:divide-x-reverse',
                                            'dark:divide-gray-700' => config('forms.dark_mode'),
                                        ])
                                    >
                                        @if ($isReorderableWithButtons)
                                            @unless ($loop->first)
                                                <li>
                                                    <button
                                                        title="{{ __('forms::components.repeater.buttons.move_item_up.label') }}"
                                                        type="button"
                                                        wire:click.stop="dispatchFormEvent('repeater::moveItemUp', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                        wire:target="dispatchFormEvent('repeater::moveItemUp', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                        wire:loading.attr="disabled"
                                                        @class([
                                                            'flex h-10 w-10 flex-none items-center justify-center text-gray-400 outline-none transition hover:text-gray-500 focus:bg-gray-500/5',
                                                            'dark:border-gray-700 dark:focus:bg-gray-600/20' => config('forms.dark_mode'),
                                                        ])
                                                    >
                                                        <span class="sr-only">
                                                            {{ __('forms::components.repeater.buttons.move_item_up.label') }}
                                                        </span>

                                                        <x-heroicon-s-chevron-up
                                                            class="h-4 w-4"
                                                            wire:loading.remove.delay
                                                            wire:target="dispatchFormEvent('repeater::moveItemUp', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                        />

                                                        <x-filament-support::loading-indicator
                                                            class="h-4 w-4 text-primary-500"
                                                            wire:loading.delay
                                                            wire:target="dispatchFormEvent('repeater::moveItemUp', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                            x-cloak
                                                        />
                                                    </button>
                                                </li>
                                            @endunless

                                            @unless ($loop->last)
                                                <li>
                                                    <button
                                                        title="{{ __('forms::components.repeater.buttons.move_item_down.label') }}"
                                                        type="button"
                                                        wire:click.stop="dispatchFormEvent('repeater::moveItemDown', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                        wire:target="dispatchFormEvent('repeater::moveItemDown', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                        wire:loading.attr="disabled"
                                                        @class([
                                                            'flex h-10 w-10 flex-none items-center justify-center text-gray-400 outline-none transition hover:text-gray-500 focus:bg-gray-500/5',
                                                            'dark:border-gray-700 dark:focus:bg-gray-600/20' => config('forms.dark_mode'),
                                                        ])
                                                    >
                                                        <span class="sr-only">
                                                            {{ __('forms::components.repeater.buttons.move_item_down.label') }}
                                                        </span>

                                                        <x-heroicon-s-chevron-down
                                                            class="h-4 w-4"
                                                            wire:loading.remove.delay
                                                            wire:target="dispatchFormEvent('repeater::moveItemDown', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                        />

                                                        <x-filament-support::loading-indicator
                                                            class="h-4 w-4 text-primary-500"
                                                            wire:loading.delay
                                                            wire:target="dispatchFormEvent('repeater::moveItemDown', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                            x-cloak
                                                        />
                                                    </button>
                                                </li>
                                            @endunless
                                        @endif

                                        @if ($isCloneable)
                                            <li>
                                                <button
                                                    title="{{ __('forms::components.repeater.buttons.clone_item.label') }}"
                                                    wire:click.stop="dispatchFormEvent('repeater::cloneItem', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                    wire:target="dispatchFormEvent('repeater::cloneItem', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                    wire:loading.attr="disabled"
                                                    type="button"
                                                    @class([
                                                        'flex h-10 w-10 flex-none items-center justify-center text-gray-400 outline-none transition hover:text-gray-500 focus:bg-gray-500/5',
                                                        'dark:border-gray-700 dark:focus:bg-gray-600/20' => config('forms.dark_mode'),
                                                    ])
                                                >
                                                    <span class="sr-only">
                                                        {{ __('forms::components.repeater.buttons.clone_item.label') }}
                                                    </span>

                                                    <x-heroicon-s-duplicate
                                                        class="h-4 w-4"
                                                        wire:loading.remove.delay
                                                        wire:target="dispatchFormEvent('repeater::cloneItem', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                    />

                                                    <x-filament-support::loading-indicator
                                                        class="h-4 w-4 text-primary-500"
                                                        wire:loading.delay
                                                        wire:target="dispatchFormEvent('repeater::cloneItem', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                        x-cloak
                                                    />
                                                </button>
                                            </li>
                                        @endunless

                                        @unless ($isItemDeletionDisabled)
                                            <li>
                                                <button
                                                    title="{{ __('forms::components.repeater.buttons.delete_item.label') }}"
                                                    wire:click.stop="dispatchFormEvent('repeater::deleteItem', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                    wire:target="dispatchFormEvent('repeater::deleteItem', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                    wire:loading.attr="disabled"
                                                    type="button"
                                                    @class([
                                                        'flex h-10 w-10 flex-none items-center justify-center text-danger-600 outline-none transition hover:text-danger-500 focus:bg-gray-500/5',
                                                        'dark:text-danger-500 dark:hover:text-danger-400 dark:focus:bg-gray-600/20' => config('forms.dark_mode'),
                                                    ])
                                                >
                                                    <span class="sr-only">
                                                        {{ __('forms::components.repeater.buttons.delete_item.label') }}
                                                    </span>

                                                    <x-heroicon-s-trash
                                                        class="h-4 w-4"
                                                        {{--  Here we add our onclick event   --}}
                                                        onclick="confirm('Are you sure you want to remove this?') || event.stopImmediatePropagation()"
                                                        wire:loading.remove.delay
                                                        wire:target="dispatchFormEvent('repeater::deleteItem', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                    />

                                                    <x-filament-support::loading-indicator
                                                        class="h-4 w-4 text-primary-500"
                                                        wire:loading.delay
                                                        wire:target="dispatchFormEvent('repeater::deleteItem', '{{ $getStatePath() }}', '{{ $uuid }}')"
                                                        x-cloak
                                                    />
                                                </button>
                                            </li>
                                        @endunless

                                        @if ($isCollapsible)
                                            <li>
                                                <button
                                                    x-bind:title="
                                                        ! isCollapsed
                                                            ? '{{ __('forms::components.repeater.buttons.collapse_item.label') }}'
                                                            : '{{ __('forms::components.repeater.buttons.expand_item.label') }}'
                                                    "
                                                    x-on:click.stop="isCollapsed = ! isCollapsed"
                                                    type="button"
                                                    @class([
                                                        'flex h-10 w-10 flex-none items-center justify-center text-gray-400 outline-none transition hover:text-gray-500 focus:bg-gray-500/5',
                                                        'dark:focus:bg-gray-600/20' => config('forms.dark_mode'),
                                                    ])
                                                >
                                                    <x-heroicon-s-minus-sm
                                                        class="h-4 w-4"
                                                        x-show="! isCollapsed"
                                                    />

                                                    <span
                                                        class="sr-only"
                                                        x-show="! isCollapsed"
                                                    >
                                                        {{ __('forms::components.repeater.buttons.collapse_item.label') }}
                                                    </span>

                                                    <x-heroicon-s-plus-sm
                                                        class="h-4 w-4"
                                                        x-show="isCollapsed"
                                                        x-cloak
                                                    />

                                                    <span
                                                        class="sr-only"
                                                        x-show="isCollapsed"
                                                        x-cloak
                                                    >
                                                        {{ __('forms::components.repeater.buttons.expand_item.label') }}
                                                    </span>
                                                </button>
                                            </li>
                                        @endif
                                    </ul>
                                </header>
                            @endif

                            <div
                                x-bind:class="{
                                    'invisible h-0 !m-0 overflow-y-hidden': isCollapsed,
                                    'p-6': ! isCollapsed,
                                }"
                            >
                                {{ $item }}
                            </div>

                            <div
                                class="p-2 text-center text-xs text-gray-400"
                                x-show="isCollapsed"
                                x-cloak
                            >
                                {{ __('forms::components.repeater.collapsed') }}
                            </div>
                        </li>
                    @endforeach
                </x-filament-support::grid>
            </ul>
        @endif

        @if (! $isItemCreationDisabled)
            <div class="relative flex justify-center">
                <x-forms::button
                    :wire:click="'dispatchFormEvent(\'repeater::createItem\', \'' . $getStatePath() . '\')'"
                    size="sm"
                    outlined
                >
                    {{ $getCreateItemButtonLabel() }}
                </x-forms::button>
            </div>
        @endif
    </div>
</x-dynamic-component>

You can search for Here we add our onclick event to easily find customization.

Second Option: It seems you tried repeater::deleteItem, but there's a point. When you define a listener with the same name, it will not override for some reason, and because of that, you still have the default behavior.

To override that listener, it tried something like this:

First, create a new repeater component class.

For example, let's create Repeater.php in app/Filament/Forms/Compontent. This class extends Filament's Repeater component class.

<?php

namespace App\Filament\Forms\Component;

class Repeater extends \Filament\Forms\Components\Repeater
{
    protected function setUp(): void
    {
        parent::setUp();
        $this->listeners['repeater::deleteItem'] = [
            function (\Filament\Forms\Components\Repeater $component, string $statePath, string $uuidToDelete): void {
            // !! below lines are default behaviour of this action and you can customize them !!
                if ($statePath !== $component->getStatePath()) {
                    return;
                }

                $items = $component->getState();
                unset($items[$uuidToDelete]);

                $livewire = $component->getLivewire();
                data_set($livewire, $statePath, $items);
            },

        ];

    }
}

Then, you need to bind Filament's Repeater to your new Repeater.

In AppServiceProvider:

use App\Filament\Forms\Component\Repeater;

// ...
    public function register(): void
    {
        // ...
        $this->app->bind(\Filament\Forms\Components\Repeater::class, Repeater::class);
    }

Now repeater::deleteItem does whatever you say.