Cómo crear un diálogo de confirmación en Vue.js

En este tutorial, veremos cómo crear un diálogo de confirmación en Vue.js, creando una ventana emergente modal general y heredándola para este propósito, con ejemplos.

Introducción

Un diálogo de confirmación es un patrón de interfaz de usuario en el que el usuario tendrá la opción de continuar con su acción o cancelarla. Se usa comúnmente con acciones destructivas o irreversibles, para asegurarse de que el usuario quiera continuar.

En este artículo, implementaremos un diálogo de confirmación modular y reutilizable en Vue.js.

Creación de un componente emergente reutilizable

Comencemos por crear un componente base reutilizable para cualquier tipo de componente emergente. De esa manera no tenemos que volver a implementar la mecánica emergente varias veces. Esto se puede reutilizar más adelante para crear cualquier cosa, desde un cuadro de alerta hasta una ventana emergente de boletín informativo.

Comencemos con la plantilla:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- components/PopupModal.vue -->

<template>
    <transition name="fade">
        <div class="popup-modal" v-if="isVisible">
            <div class="window">
                <slot></slot>
            </div>
        </div>
    </transition>
</template>

Observe que agregamos una etiqueta <slot></slot> vacía a la plantilla. Esta etiqueta nos permite insertar cualquier contenido en el elemento PopupModal en la etiqueta <slot></slot>. Para leer más sobre cómo funcionan las tragamonedas, consulte la Guía Vue sobre tragamonedas.

También agregamos la etiqueta <transition name="fade"> a la plantilla. Usaremos esto en la siguiente sección para animar un efecto de aparición/desaparición gradual en el diálogo.

Luego, agregaremos las funciones de evento data(), open() y close():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- components/PopupModal.vue -->

<script>
export default {
    name: 'PopupModal',

    data: () => ({
        isVisible: false,
    }),

    methods: {
        open() {
            this.isVisible = true
        },

        close() {
            this.isVisible = false
        },
    },
}
</script>

Y finalmente, agreguemos algo de estilo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- components/PopupModal.vue -->

<style scoped>
/* css class for the transition */
.fade-enter-active,
.fade-leave-active {
    transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
    opacity: 0;
}

.popup-modal {
    background-color: rgba(0, 0, 0, 0.5);
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 0.5rem;
    display: flex;
    align-items: center;
    z-index: 1;
}

.window {
    background: #fff;
    border-radius: 5px;
    box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);
    max-width: 480px;
    margin-left: auto;
    margin-right: auto;
    padding: 1rem;
}
</style>

Animación de diálogo de confirmación

En la etiqueta de la plantilla, verá una etiqueta de transición <transition name="fade">. Esto se usa para animar estados simples de entrada/salida. Cualquier cosa dentro de esta etiqueta se animará si se agregó o eliminó de la etiqueta.

Estamos usando un v-if="isVisible" condicional para ocultar y mostrar la ventana emergente. Puede leer más al respecto en la Guía de Vue sobre transiciones.

Para especificar cómo transiciona el contenido, hemos llamado a nuestra animación fade. Para implementar esta transición en CSS, agregaremos clases con el prefijo fade, que coincidan con nuestro atributo name de la etiqueta <transition>.

Todo lo que hace es animar la opacidad cuando la ventana emergente se cierra y se abre:

1
2
3
4
5
6
7
8
.fade-enter-active,
.fade-leave-active {
    transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
    opacity: 0;
}

Heredar el componente emergente

Para crear nuestro diálogo de confirmación, heredaremos el PopupModal por composición y personalizaremos la ventana modal reutilizable para que se convierta en un diálogo de confirmación.

Vamos a crear un nuevo archivo, components/ConfirmDialogue.vue y definir una plantilla dentro de él:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- components/ConfirmDialogue.vue -->

<template>
    <popup-modal ref="popup">
        <h2 style="margin-top: 0">{{ title }}</h2>
        <p>{{ message }}</p>
        <div class="btns">
            <button class="cancel-btn" @click="_cancel">{{ cancelButton }}</button>
            <span class="ok-btn" @click="_confirm">{{ okButton }}</span>
        </div>
    </popup-modal>
</template>

Debido a que definimos la etiqueta <slot></slot> en el componente popup-modal, todo lo que coloquemos entre las etiquetas de sus componentes (<popup-modal></popup-modal>) se representará entre sus etiquetas <slot> en su lugar.

También agregamos un ref="popup" a la etiqueta popup-modal. Al configurar ese atributo, ahora podemos acceder a la instancia popup-modal con this.$refs.popup. Usaremos esa referencia para llamar a open() y close() en el modal emergente.

Luego, implementemos los métodos del componente principal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!-- components/ConfirmDialogue.vue -->

<script>
import PopupModal from './PopupModal.vue'

export default {
    name: 'ConfirmDialogue',

    components: { PopupModal },

    data: () => ({
        // Parameters that change depending on the type of dialogue
        title: undefined,
        message: undefined, // Main text content
        okButton: undefined, // Text for confirm button; leave it empty because we don't know what we're using it for
        cancelButton: 'Go Back', // text for cancel button
        
        // Private variables
        resolvePromise: undefined,
        rejectPromise: undefined,
    }),

    methods: {
        show(opts = {}) {
            this.title = opts.title
            this.message = opts.message
            this.okButton = opts.okButton
            if (opts.cancelButton) {
                this.cancelButton = opts.cancelButton
            }
            // Once we set our config, we tell the popup modal to open
            this.$refs.popup.open()
            // Return promise so the caller can get results
            return new Promise((resolve, reject) => {
                this.resolvePromise = resolve
                this.rejectPromise = reject
            })
        },

        _confirm() {
            this.$refs.popup.close()
            this.resolvePromise(true)
        },

        _cancel() {
            this.$refs.popup.close()
            this.resolvePromise(false)
            // Or you can throw an error
            // this.rejectPromise(new Error('User cancelled the dialogue'))
        },
    },
}
</script>

Finalmente, agreguemos algo de estilo para que se vea un poco mejor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- components/ConfirmDialogue.vue -->

<style scoped>
.btns {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
}

.ok-btn {
    color: red;
    text-decoration: underline;
    line-height: 2.5rem;
    cursor: pointer;
}

.cancel-btn {
    padding: 0.5em 1em;
    background-color: #d5eae7;
    color: #35907f;
    border: 2px solid #0ec5a4;
    border-radius: 5px;
    font-weight: bold;
    font-size: 16px;
    text-transform: uppercase;
    cursor: pointer;
}
</style>

Usando el diálogo de confirmación

Para usar el diálogo de confirmación, debe incluir solo el componente components/ConfirmDialogue.vue. Por ejemplo, hagamos una página con un botón 'Eliminar' que asegure si realmente desea eliminar otra página:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
    <div>
        <h1>Delete Page</h1>
        <button class="delete-btn" @click="doDelete">Delete Page</button>
        <confirm-dialogue ref="confirmDialogue"></confirm-dialogue>
    </div>
</template>

<script>
import ConfirmDialogue from '../components/ConfirmDialogue.vue'

export default {
    components: { ConfirmDialogue },
    methods: {
        async doDelete() {
            const ok = await this.$refs.confirmDialogue.show({
                title: 'Delete Page',
                message: 'Are you sure you want to delete this page? It cannot be undone.',
                okButton: 'Delete Forever',
            })
            // If you throw an error, the method will terminate here unless you surround it wil try/catch
            if (ok) {
                alert('You have successfully delete this page.')
            } else {
                alert('You chose not to delete this page. Doing nothing now.')
            }
        },
    },
}
</script>

<style scoped>
.delete-btn {
    padding: 0.5em 1em;
    background-color: #eccfc9;
    color: #c5391a;
    border: 2px solid #ea3f1b;
    border-radius: 5px;
    font-weight: bold;
    font-size: 16px;
    text-transform: uppercase;
    cursor: pointer;
}
</style>

Como estamos usando await en nuestro método para obtener el resultado del diálogo de confirmación, necesitamos agregar async a la definición de nuestro método.

Alternativamente, puede preferir el enfoque de estilo de promesa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
this.$refs.confirmDialogue.show({
    title: 'Delete Page',
    message: 'Are you sure you want to delete this page? It cannot be undone.',
    okButton: 'Delete Forever',
}).then((result) => {
    if (ok) {
        alert('You have successfully delete this page.')
    } else {
        alert('You chose not to delete this page. Doing nothing now.')
    }
})

Para ver por qué sugerimos arrojar un error si el usuario cancela el diálogo de confirmación, vea qué tan fluido es el siguiente código:

1
2
3
4
5
6
await this.$refs.confirmDialogue.show({
    title: 'Delete Page',
    message: 'Are you sure you want to delete this page? It cannot be undone.',
    okButton: 'Delete Forever',
})
alert('Deleting this page.')

Dado que la cancelación no requiere ninguna acción, simplemente no hay necesidad de manejar ese estado en absoluto. Y si decide manejar una solicitud de cancelación, simplemente envuelva ese código con try/catch.

Conclusión

En este artículo, definimos un componente emergente modal reutilizable en Vue.js y lo heredamos para implementar un diálogo de confirmación. Luego, le agregamos animaciones con fines estéticos y ejecutamos un par de ejemplos de cómo usar el componente para solicitar a los usuarios que ingresen.