vue3-promise-dialog

Dialogs meet promises in Vue 3 !

This project does not provide any dialogs. Rather, it makes it easy to create your own dialogs and work with them using
promises. The dialogs can be opened by calling a function that returns a promise and once the user enters data into the
dialog and closes it, the promise resolves with the data the user entered.

let data = await openMyDialog();

Installation

npm i vue3-promise-dialog

Demos

Showcases the small dialog collection included in this repository as examples :

https://rlemaigre.github.io/vue3-promise-dialog/

A Vite + Vue 3 + Typescript project on Stackblitz featuring a confirm dialog which is probably the simplest use case of the library :

https://stackblitz.com/edit/vitejs-vite-nzzfdg?&terminal=dev

Introduction

Dialogs the usual way

If you need to use a dialog in a parent component, you usually do it by altering its script and template, something
along those lines :

<template>
    <transition name="...">
        <ConfirmDialog v-if="show" @ok="onOk()" @cancel="onCancel()"></ConfirmDialog>
    </transition>
</template>
<script>
    // Set up a show ref, onOk and onCancel functions. Switch show to true to open the dialog.
</script>

That approach has several disadvantages :

  • It is verbose.
  • A dialog can only be opened from a parent Vue component, not from a JS/TS file.
  • There is no symmetry between requesting data from the user and from the server, yet it is the same kind of
    asynchronous process that yields a value.
  • Everywhere you need to use the dialog, you need to set up some logic in the parent component :
    • The dialog tag in the template
    • A ref that controls the dialog visibility
    • Callbacks that handle clicks on dialog buttons
  • Things get nasty when a parent component needs to use several dialogs.

Dialogs using promises

As we all know, requesting data from the server is an asynchronous process that is best handled with promises. For
example the Fetch API is a promise based API.

let response = await fetch('http://example.com/movies.json');

Now, just like fetching data from the server, requesting data from the user using a dialog is also an asynchronous
process that may complete with a value at some point in the future when the user closes the dialog. Why would the API to
fetch data from the user be any different than the API to fetch data from the server ?

Using promises, opening a confirm dialog (for example) is as simple as this :

let ok = await confirm('Are you sure you want to do this ?');
if (ok) {
    // Do something
}

The promise resolves to the value the user entered into the dialog when it is closed.

That approach has several advantages :

  • It is concise.
  • A dialog can be opened from a parent Vue component, or from any JS/TS file. It’s just a function call.
  • There is a pleasing symmetry between requesting data from the user and from the server.
  • No need to alter the code of a parent component to accommodate for the presence of the dialog.
  • A parent component can use many dialogs without becoming a mess.

Content of this repository

You may be familiar with the following Vue 2 project : vue-modal-dialog.
Unfortunately it hasn’t been ported to Vue 3. This repository demonstrates how the basic functionality of that project
can be easily recovered in Vue 3.

Directory structure

The core functionality is in the lib folder.

An example of a small dialog collection built upon the core functionality is in the src/dialogs folder. It is not
published on NPM since it is dependent on PrimeVue (for buttons and text fields) which you may not be using and the look
and feel you may be aiming for for your own dialogs may differ. Use it as inspiration to build your own dialog
collection.

Using the library

Plugin

Install the plugin like this :

app.use(PromiseDialog);

The plugin defines the $close global function and makes it available in all components.

DialogWrapper

Your dialogs will open inside a DialogWrapper component. Include the DialogWrapper component at the root of your vue
app, after all other content. Internally DialogWrapper uses a transition tag to transition your dialogs in and out of
view. Use the transitionAttrs prop to control the transition : the value of that prop will be v-binded as is to the
transition tag inside the wrapper. So for example to set the name of the transition to dialog,
use :transition-attrs="{name: 'dialog'}".

<template>
    <div id="app">
        <!-- your content -->
        <DialogWrapper :transition-attrs="{name: 'dialog'}"/>
    </div>
</template>

Dialog

A dialog is just a standard component that may be transitioned into view. It may be a simple div that you center with
position: fixed. You may add a dark overlay below it to make the div stand out. Whatever. To animate the dialog
appearance and disappearance, use the CSS class names applied by Vue according to the name of the transition defined by
DialogWrapper (dialog in my example, so dialog-enter-from, dialog-leave-to and so on).

Opening dialogs

To open a dialog X passing it some props, call openDialog(X, props). The function will return a promise that will
resolve when the dialog is closed.

let result = await openDialog(MyDialog, myProps);

If you wish to make the call more user-friendly, you may wrap the openDialog function call into another function that hides the details of the dialog component in use. For example, to open a confirm dialog, you may define a confirm function :

export async function confirm(text: string) {
    return await openDialog(ConfirmDialog, {text});
}

let ok = await confirm("Are you sure ?");

Closing dialogs

Your dialog must define a returnValue function. You may do so either in the setup function using Composition API or as
a method using Options API. To close the dialog, call $close(this). When you do so, the promise will resolve to the
result of the returnValue function. You may also resolve the promise to something else (for example null) by passing a
value as second argument to $close.

Typescript

The openDialog function will infer the types of the props and the return type from the component definition. It is
type safe. Your IDE will complain if you pass in a wrong prop or assign the result to a variable of the wrong type.

Dialog collection

Although none of this is published on NPM for reasons mentioned above, this section describes briefly the few dialogs
that serve as test case for this project.

Box.vue

The Box.vue component defines the look and feel of all dialogs : a white div centered with a dark overlay background. It
also defines the way dialogs transition in and out of view, with a fade-in effect for the background and a scale effect
for the centered div. It has one slot which is the content of the centered div.

OkCancelBox.vue

The OkCancelBox.vue component is a Box that serves as base for all dialogs that include OK and CANCEL buttons. It has
two slots : header and body. Body is where the controls of the dialog reside. It has a valid prop. If valid is
false, the OK button is disabled. The whole thing is included into a form tag so that hitting enter when a control has
focus triggers a click on the OK button. When OK is clicked, $close is called on the parent component (the dialog).
When CANCEL is clicked, $close is called on the parent component (the dialog) with a null return value.

ConfirmDialog.vue

A ConfirmDialog is an OkCancelBox with a label and a returnValue function that returns always true. So the promise
resolves to true if the user clicks OK and to null if the use clicks CANCEL.

TextDialog.vue

A TextBox is an OkCancelBox with a text field and a returnValue function that returns the content of the text field.
If the text field is empty, the valid prop is set to false on OkCancelBox.

GitHub

View Github