GrDialogService

Package: @feugene/granularitycoreGroup: misc

Machine-translated from the Russian original, not yet reviewed. Read the original

When to take it

  • the window is called from code — from a handler, from a store, from a request interceptor: no markup in the template is needed at all;
  • the result is needed as a valueconfirm/prompt return a Promise, and the branching is written linearly;
  • the window is opened from a place with no template — a .ts module, a router guard, an error handler;
  • there are many windows and they are alike — a shared host instead of a copy of v-model in every component.

When to take something else

NeedTake
The window is opened by markup and lives on the screenGrDialog
The confirmation is declared in the templateGrConfirmDialog
The request for a value is declared in the templateGrPromptDialog
A message without a questionGrToaster
A layout of your own for the modal layerGrModal

How the promise resolves

The high-level methods never reject: a cancellation is a regular outcome, not an exception. confirm gives a boolean, prompt a string | null, alert a void. If the exact reason for the closing is needed — open(), which resolves { action: 'confirm' | 'cancel' | 'close', value? }.

Every promise is extended with a close() method: closing the dialog from code.

Asynchronous confirmation

confirm and prompt accept an onConfirm, which receives a context and may be asynchronous. While it is in flight:

  • the confirm button shows loading;
  • Esc and a click on the backdrop are switched off — an accidental movement must not cut the operation short. The close button in the header remains: a window with a hanging request needs an explicit way out;
  • the dialog stays open if the callback returned false, set an error or threw an exception.

The context provides value, signal (aborted when the dialog is closed any way — by a button, by the promise’s close(), by closeAll()), setError / setFieldError / clearErrors, setRawError, setLoading, close.

setFieldError(field, message) is addressed: the errors are collected into a map by field names. GrPromptDialog shows the error of its own field (value), and if there is a single entry — that one, whatever name it was marked with.

Server errors

Instead of parsing by hand, onConfirm passes the raw answer (a Response, an axios/fetch error, an Error, a string, JSON) to ctx.setRawError. The service runs it through the same chain of parsers as GrResponseErrorBanner: the general message is drawn in the body of the dialog, fieldErrors are laid out across the fields, and the window stays open for another attempt. An exception from onConfirm is classified automatically if the error has not been set by hand.

The default chain is the universal core (coreResponseErrorParsers): abort, network, HTTP status, plain message. They work at the transport level and make no format-specific assumptions, so they do not fire falsely on almost-Laravel answers. Server-specific parsers (Laravel, RFC 7807, JSON:API) are connected deliberately through errorParsers — as an array or as a builder function that receives the named presets.

The application context

The host — GrDialogServiceHost — is mounted with a separate render() into document.body, outside the component tree. Its export is needed by the plugin and by tests: there is no need to put the host into a template by hand, the service does that itself.

Vue takes provides only from appContext, which receives nothing but app.provide(), so the values from <GrConfigProvider> do not reach the host. That is why the config and the i18n are captured at the point of the call and travel together with the request.

The priority: the appContext option of the call → an explicit setAppContext → the auto-cache from the first call to useDialogService() inside setup.

The ready singleton dialogService is created when the module is imported, where inject does not work at all. It takes the context of the last call to useDialogService() from setup — that is enough for an ordinary application, but with two subtrees with different providers the choice will belong to the last one. If precision is needed, call useDialogService() in setup or set setAppContext. A dialog opened with no context at all prints a warning in a dev build.

Isolating applications

app.use(granularityDialogServicePlugin)

The plugin gives the application a queue and a host of its own and removes them on app.unmount(). It is required where there are several applications on the page (micro-frontends — otherwise they share one queue), and useful with HMR, where the container of the previous application would otherwise keep hanging in document.body.

Without the plugin the service works as before — on lazy module state; for an ordinary SPA that is exactly what is needed.

The dialogService singleton cannot inject, so it chooses the state like this: if there is a single one registered by the plugin — that one; if there are several — a dev warning and the module fallback, because there is nothing here to choose on the author’s behalf. The second case is cured by calling useDialogService() in the setup of the application in question.

teardownDialogService() remains a manual way out and works over the same state as the service itself.

The queue

Ordinary calls are serialised by a FIFO queue: the head is shown, and the next dialog follows after the previous one is closed. Three alerts from a loop will not lie in a stack, and nobody competes for the focus trap. closeAll() dismantles the queue in the same FIFO order — the promises resolve in the order of the calls.

priority (0 by default) moves a request among the waiting ones: higher means earlier, and on a tie the order of the calls holds. The window already shown is not interrupted in the process — pulling the focus trap out from under the user is worse than a delay of one window.

Three things can finish a request: a button in the window, the close() of its promise and closeAll(). The path is one and idempotent: finishing again is a no-op, the promise resolves exactly once, and everything started for the request (a subscription to an external signal, an AbortController) is wound up when the window leaves the screen.

Nested dialogs

A dialog opened from the onConfirm of another dialog is shown on top of it rather than joining the queue:

await dialog.confirm('Delete the project?', {
  async onConfirm() {
    return await dialog.confirm('Are you sure? There will be nothing to restore from.')
  },
})

Otherwise the nested call would wait in the queue for the one that is waiting for it: the outer window would hang in loading and the promise would never resolve. A stack of windows here is not an ornament but the only way not to get a deadlock.

Esc, inert and the return of focus come for free from the shared layer stack: the top window closes, the lower one is marked inert, and the focus returns into it.

The lifecycle and SSR

The host is mounted lazily on the first call and lives as long as its state does: with the plugin — until app.unmount(), without the plugin — until teardownDialogService().

The service is client-only: without window/document the methods throw rather than work idly — otherwise the module queue would be mutated on the server and would leak between requests. Hide imperative calls behind a client-side check.

Accessibility

All of the guarantees come from the reusable stack of GrConfirmDialog/GrPromptDialog/GrDialog/GrModal: a single heading per window, a focus trap, the return of focus. Esc and the backdrop follow closeOnEsc/closeOnBackdrop and go into the cancel/close branch.

Install

npm i @feugene/granularity

Import

import { GrDialogService } from '@feugene/granularity/components/GrDialogService'

API

The API for this component has not been generated yet: the showcase generator only covers the core so far. Until it does, the reference lives in the package documentation.

Examples 2

Link

Linkdepends on the showcase environment
<script setup lang="ts">
import { RouterLink } from 'vue-router'

import { GrButton } from '@feugene/granularity'
</script>

<template>
  <div class="grid gap-3">
    <p class="text-sm text-[var(--gr-muted-fg)]">
      Императивные вызовы диалогов (confirm / prompt / alert) из script/ts-секции без вставки
      компонента в шаблон вынесены на отдельную страницу сервиса useDialogService.
    </p>
    <RouterLink to="/composables/use-dialog-service" class="justify-self-start">
      <GrButton variant="primary">
        Открыть страницу useDialogService
      </GrButton>
    </RouterLink>
  </div>
</template>

Link

Linkdepends on the showcase environment
<script setup lang="ts">
import { RouterLink } from 'vue-router'

import { GrButton } from '@feugene/granularity'
</script>

<template>
  <div class="grid gap-3">
    <p class="text-sm text-[var(--gr-muted-fg)]">
      Императивные вызовы диалогов (confirm / prompt / alert) из script/ts-секции без вставки
      компонента в шаблон вынесены на отдельную страницу сервиса useDialogService.
    </p>
    <RouterLink to="/composables/use-dialog-service" class="justify-self-start">
      <GrButton variant="primary">
        Открыть страницу useDialogService
      </GrButton>
    </RouterLink>
  </div>
</template>

Component documentationAll components