We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
2022 / 08 / 01
A confirmation dialog component with Vue 3 and Tailwind CSS
Let's write a reusable confirmation dialog with Vue and Tailwind CSS.
Prerequisites
We’ll use the <BaseModal>
component we wrote in:
A basic modal component with Vue 3 and Tailwind CSS.
Desired API
Let’s suppose we already have a <ConfirmationDialog>
component,
and we’d like to use it like this:
Dialog 1
<script setup lang="ts">
import { ref } from 'vue'
import ConfirmationDialog from '@/components/modals/ConfirmationDialog.vue'
const showDialog = ref(false)
function handleResult(value: boolean) {
showDialog.value = false
// Do something with `value`
console.log('value', value)
}
</script>
<template>
<ConfirmationDialog :show="showDialog1" @result="handleResult" />
</template>
Here is where you can think about and define the API you want to provide for users of your component.
Is it better to have a @result
event that gets passed a boolean?
Or would it better to have two type of events emitted like @confirm
and @cancel
?
That depends on your taste. For now I’ll go with the simple approach of having a single event that we can handle on the host.
Customizability
Ideally we’d like to have a default implementation that’s good enough for the majority of use cases; but then we’d like to have the option to provide alternative markup for the different parts that compose our component.
This is the perfect use case for slots in Vue.
Let’s say we want to customize the title and body, maybe with
something like this:
Dialog 2
<script setup lang="ts">
import { ref } from 'vue'
import ConfirmationDialog from '@/components/modals/ConfirmationDialog.vue'
const showDialog = ref(false)
function handleResult(value: boolean) {
showDialog.value = false
// Do something with `value`
console.log('value', value)
}
</script>
<template>
<ConfirmationDialog :show="showDialog" @result="handleResult">
<template #title="{ emitResult }">
<div class="flex justify-between">
<div class="text-xl font-semibold tracking-wide">
Cool confirmation
</div>
<CloseIcon
class="w-6 cursor-pointer text-pink-600"
@click="emitResult(false)"
/>
</div>
</template>
<template #body>
<div class="py-4 text-sm">Are you super duper sure about THIS?</div>
</template>
</ConfirmationDialog>
</template>
Again, if you use a named slot for the body or the default slot comes down to taste and the type of API you want to provide.
In my case I will provide named slots for all the parts, and a default slot to override the whole component.
What about customizing the dialog buttons?
Another easy one:
Dialog 3
<script setup lang="ts">
import { ref } from 'vue'
import ConfirmationDialog from '@/components/modals/ConfirmationDialog.vue'
const showDialog = ref(false)
function handleResult(value: boolean) {
showDialog.value = false
// Do something with `value`
console.log('value', value)
}
</script>
<template>
<ConfirmationDialog :show="showDialog3" @result="handleResult">
<template #actions="{ emitResult }">
<div class="flex gap-3">
<button
class="border border-red-400 bg-gray-200 px-3 font-semibold uppercase"
@click="emitResult(false)"
>
No way!
</button>
<button
class="border border-cyan-400 bg-gray-200 px-3 font-semibold uppercase"
@click="emitResult(true)"
>
Yes, please
</button>
</div>
</template>
</ConfirmationDialog>
</template>
But, what if we want to completely override the design
of the confirmation dialog?
WDYT? :thinking:
Dialog 4
<script setup lang="ts">
import { ref } from 'vue'
import ConfirmationDialog from '@/components/modals/ConfirmationDialog.vue'
const showDialog = ref(false)
function handleResult(value: boolean) {
showDialog.value = false
// Do something with `value`
console.log('value', value)
}
</script>
<template>
<ConfirmationDialog
v-slot="{ emitResult }"
:show="showDialog"
@result="handleResult"
>
<div class="bg-black p-3 text-lg font-bold text-white">
A very custom title
</div>
<div class="max-w-sm p-4">
<div class="text-sm">
Do you REALLY agree to the terms of the agreement presented in here?
</div>
<div class="flex justify-between pt-5">
<button
class="bg-pink-600 px-4 py-2 text-sm font-bold text-white"
@click="emitResult(false)"
>
NOPE NOPE NOPE
</button>
<button
class="bg-sky-600 px-4 py-2 text-sm font-bold text-white"
@click="emitResult(true)"
>
YEP
</button>
</div>
</div>
</ConfirmationDialog>
</template>
So many possibilities! :thinking: :dizzy:
Source code
Let me show you the implementation I used for the four demos
above.
src/components/modals/ConfirmationDialog.vue
:
<script setup lang="ts">
import BaseModal from '@/components/modals/BaseModal.vue'
defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: 'result', value: boolean): void
}>()
// We might want to delegate the process of emitting
// the result to somewhere else, so we define a function
// we can pass through scoped slots
function emitResult(value: boolean) {
emit('result', value)
}
</script>
<template>
<BaseModal :show="show">
<!-- Default slot, when we want to override the whole component -->
<slot :emit-result="emitResult">
<div class="p-4">
<!-- Title slot, we pass the `emitResult` in case
we add a close button or something to it -->
<slot name="title" :emit-result="emitResult">
<div class="text-lg font-medium">Please confirm</div>
</slot>
<!-- Body slot to customize the content -->
<slot name="body">
<div class="py-2 text-sm">Are you sure?</div>
</slot>
<!-- Actions slot, to customize the dialog buttons -->
<slot name="actions" :emit-result="emitResult">
<div class="flex justify-end gap-2">
<button
class="bg-indigo-200 px-3 py-1 font-medium"
@click="$emit('result', false)"
>
Cancel
</button>
<button
class="bg-indigo-200 px-3 py-1 font-medium"
@click="$emit('result', true)"
>
Ok
</button>
</div>
</slot>
</div>
</slot>
</BaseModal>
</template>
That’s it! :tada: