Índice de contenido
Los diálogos de confirmación es simplemente un modal o ventana emergente en la cual se ejecuta para mostrar antes de realizar alguna acción como la de eliminar y en la cual podemos colocar otros datos mediante por ejemplo, enlaces en Laravel Inertia.
En este apartado, quiero mostrarte la forma oficial de utilizar los diálogos o modales de confirmación. Aunque podríamos usar el confirm() nativo de JavaScript, no es lo ideal estéticamente. En el video anterior utilizamos un paquete externo, pero ahora quiero enseñarte cómo integrarlo directamente en la interfaz.
Comparativa: Inertia.js vs. Livewire (Flux)
Uno de los puntos que menos me gusta de Inertia es que, a veces, genera mucho "ruido" con tantos componentes. Creo que deberían trabajar más en la reutilización, ya que un diálogo de confirmación es un elemento esencial en cualquier sistema.
Si lo comparamos con Livewire (específicamente usando Flux), la diferencia es clara:
- En Livewire/Flux, la implementación es directa y genérica. Declaras el componente y lo accionas con un simple clic.
<flux:modal.trigger name="edit-profile"> <flux:button>Edit profile</flux:button> </flux:modal.trigger> <flux:modal name="edit-profile" class="md:w-96"> <div class="space-y-6"> <div> <flux:heading size="lg">Update profile</flux:heading> <flux:text class="mt-2">Make changes to your personal details.</flux:text> </div>
- En Inertia, no tenemos uno genérico por defecto; tenemos que construirlo nosotros mismos integrando componentes de terceros o de la propia interfaz (como los de Headless UI o Radix).
Es un proceso algo tortuoso tener que hacer esto manualmente cada vez. Por eso, vamos a crear un componente genérico que podamos adaptar fácilmente, similar a cómo funciona en otras tecnologías.
Estructura del Componente de Diálogo
Aquí te explico cómo funciona nuestro componente reutilizable:
Definición de Props y Eventos
El componente recibe datos clave mediante props para ser flexible:
- Title: El título del modal.
- Operation: La acción que se va a realizar.
- Confirmation Name: El texto o nombre del ítem que queremos validar.
- Item Name: El label del recurso (por ejemplo, "Post" o "Categoría").
resources\js\components\ConfirmDeleteModal.vue
<script setup lang="ts">
import { Form } from '@inertiajs/vue3';
import { ref } from 'vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
type Props = {
open: boolean;
title: string;
itemName: string;
itemLabel: string;
deleteRoute: object;
};
const props = defineProps<Props>();
const emit = defineEmits<{
'update:open': [value: boolean];
}>();
const confirmationName = ref('');
const formKey = ref(0);
const handleOpenChange = (nextOpen: boolean) => {
emit('update:open', nextOpen);
if (!nextOpen) {
confirmationName.value = '';
formKey.value++;
}
};
</script>
<template>
<Dialog :open="props.open" @update:open="handleOpenChange">
<DialogContent>
<Form
:key="formKey"
v-bind="deleteRoute"
class="space-y-6"
v-slot="{ processing }"
@success="handleOpenChange(false)"
>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently
delete the {{ itemLabel }}
<strong>"{{ itemName }}"</strong>.
</DialogDescription>
</DialogHeader>
<DialogFooter class="gap-2">
<DialogClose as-child>
<Button variant="secondary"> Cancel </Button>
</DialogClose>
<Button
data-test="delete-confirm"
variant="destructive"
type="submit"
:disabled="processing"
>Delete {{ itemLabel }}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
</template>Lógica de Reactividad (Composition API)
Utilizamos la lógica de Vue 3 para controlar si el diálogo está abierto o cerrado mediante un evento update:open. Cuando el usuario hace clic en aceptar, el componente hijo emite un evento al padre para que este ejecute la petición de eliminación (usualmente un router.delete de Inertia).
Opcional: Confirmar mediante texto
En este otro ejemplo, puedes ver una validación de seguridad extra: el botón de eliminar se mantiene deshabilitado hasta que el usuario escriba correctamente el nombre del recurso (título). Esto evita eliminaciones accidentales.
resources\js\components\ConfirmDeleteTextConfirmModal.vue
<script setup lang="ts">
import { Form } from '@inertiajs/vue3';
import { computed, ref } from 'vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
type Props = {
open: boolean;
title: string;
itemName: string;
itemLabel: string;
deleteRoute: object;
};
const props = defineProps<Props>();
const emit = defineEmits<{
'update:open': [value: boolean];
}>();
const confirmationName = ref('');
const formKey = ref(0);
const canDelete = computed(() => {
return (
confirmationName.value.toLowerCase() === props.itemName.toLowerCase()
);
});
const handleOpenChange = (nextOpen: boolean) => {
emit('update:open', nextOpen);
if (!nextOpen) {
confirmationName.value = '';
formKey.value++;
}
};
</script>
<template>
<Dialog :open="props.open" @update:open="handleOpenChange">
<DialogContent>
<Form
:key="formKey"
v-bind="deleteRoute"
class="space-y-6"
v-slot="{ errors, processing }"
@success="handleOpenChange(false)"
>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently
delete the {{ itemLabel }}
<strong>"{{ itemName }}"</strong>.
</DialogDescription>
</DialogHeader>
<div class="space-y-4 py-4">
<div class="grid gap-2">
<Label for="confirmation-name">
Type
<strong>"{{ itemName }}"</strong> to confirm
</Label>
<Input
id="confirmation-name"
name="confirmation"
data-test="delete-confirmation-name"
v-model="confirmationName"
placeholder="Enter name"
autocomplete="off"
/>
<InputError :message="errors.confirmation" />
</div>
</div>
<DialogFooter class="gap-2">
<DialogClose as-child>
<Button variant="secondary"> Cancel </Button>
</DialogClose>
<Button
data-test="delete-confirm"
variant="destructive"
type="submit"
:disabled="!canDelete || processing"
>
Delete {{ itemLabel }}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
</template>Utilizar
Integramos en el listado de post:
resources\js\pages\dashboard\post\Index.vue
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
***
const openDeleteModal = (post: { id: number; title: string }) => {
postToDelete.value = post;
deleteModalOpen.value = true;
};
***
<DropdownMenuItem
class="cursor-pointer text-destructive focus:text-destructive"
@click="
openDeleteModal({
id: row.id,
title: row.title,
})
"
>
<Trash2 class="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
***
<ConfirmDeleteModal
v-if="postToDelete"
:open="deleteModalOpen"
:title="postToDelete.title"
:item-name="postToDelete.title"
item-label="post"
:delete-route="destroy.form(postToDelete.id)"
@update:open="deleteModalOpen = $event"
/>Diálogos de confirmación y mensajes toast
Inertia ofrece varios componentes interesantes, pero, como podrás darte cuenta, se echan de menos algunos otros como para manejar diálogos de confirmación y mensajes toast; sin embargo, el hecho que no exista es algo bueno, ya que, Inertia al usar Vue, ya contamos con una gran cantidad de plugins listos para usar como hemos demostrado en los anteriores capítulos y con esto, podemos personalizar nuestra aplicación y prescindir de lo más posible de integraciones que pudieran venir por defecto en Inertia.js
Para los diálogos y toasts, puedes usar cualquier plugin, pero, al tener instalado Oruga UI; vamos a aprovechar los componentes que ya existen en la misma y con esto, evitar instalar paquetes adicionales.
Configurar el diálogo de confirmación
Como vimos en el libro básico de Laravel, en Oruga UI, contamos con un componente de modal:
https://oruga.io/components/modal.html
Que luce como:

Que en definitiva no es más que un contenedor vacío en el cual, podemos colocar textos, imágenes, formularios y, en definitiva, cualquier contenido HTML.
Diálogo de confirmación para eliminar
Comencemos adaptando el diálogo de confirmación para la eliminación de los posts; para ello, vamos a colocar el texto de confirmación y los botones de acción; uno para eliminar y otro para cancelar:
resources/js/pages/dashboard/category/Index.vue
<template>
<Head title="Categories" />
<o-modal v-model:active="confirmDeleteActive">
<p class="p-4 text-black">Are you sure you want to delete the selected record?</p>
<div class="flex flex-row-reverse gap-2 bg-gray-100 p-3">
<o-button variant="danger" @click="deleteCategory">Delete</o-button>
<o-button @click="confirmDeleteActive = false">Cancel</o-button>
</div>
</o-modal>
***
<DropdownMenuSeparator />
<DropdownMenuItem as-child>
<Button
variant="destructive"
@click="confirmDeleteActive = true; deleteCategoryRow = category.id"
class="w-full"
>
<Trash2 class="mr-2 h-4 w-4" />
Delete
</Button>
</DropdownMenuItem>
<!-- <Form
v-bind="destroy.form(category.id)"
v-slot="{ processing }"
>
<DropdownMenuItem
class="cursor-pointer text-destructive focus:text-destructive"
:disabled="processing"
as="button"
type="submit"
>
<Trash2 class="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</Form> -->
<script>
import { ref } from 'vue';
***
const confirmDeleteActive = ref(false);
const deleteCategoryRow = ref<number | string>(""); // Guardamos el ID aquí
const deleteCategory = () => {
// 1. Usamos .value para obtener el ID guardado
// 2. Ejecutamos la acción de borrado
router.delete(destroy(deleteCategoryRow.value).url, {
preserveScroll: true,
onSuccess: () => {
confirmDeleteActive.value = false;
deleteCategoryRow.value = "";
}
});
};
</script>Recuerda que todos estos cambios se aplican sobre:
resources/js/pages/dashboard/category/Index.vue
Para mostrar el modal, usamos una propiedad booleana y para manejar el registro que queremos eliminar, usamos otra propiedad que contiene el id del registro que queremos eliminar; así que, creamos las propiedades respectivas:
const confirmDeleteActive = ref(false);
const deleteCategoryRow = ref<number | string>(""); // Guardamos el ID aquíDesde el apartado de acciones en el listado, ahora, la operación para eliminar, lo único que va a realizar es, registrar el ID del post que se quiere eliminar:
Creamos una función que elimine el post:
const deleteCategory = () => {
// 1. Usamos .value para obtener el ID guardado
// 2. Ejecutamos la acción de borrado
router.delete(destroy(deleteCategoryRow.value).url, {
preserveScroll: true,
onSuccess: () => {
confirmDeleteActive.value = false;
deleteCategoryRow.value = "";
}
});
};Y finalmente, volviendo a los botones de acción, el de cancelar, lo único que hace es ocultar el modal con la propiedad que se encarga de indicar si el modal es visible o no:
<o-button @click="confirmDeleteActive = false">Cancel</o-button>Y el de conformación, llama a la función definida anteriormente:
<o-button variant="danger" @click="deleteCategory">Delete</o-button>Y desde el listado:
<DropdownMenuSeparator />
<DropdownMenuItem as-child>
<Button
variant="destructive"
@click="confirmDeleteActive = true; deleteCategoryRow = category.id"
class="w-full"
>
<Trash2 class="mr-2 h-4 w-4" />
Delete
</Button>
</DropdownMenuItem> Con esto, al presionar sobre eliminar en el listado, tendremos:

El siguiente paso es que aprendas a usar los Mensajes Flash en Laravel Inertia.