GrDialogService

Пакет: @feugene/granularityядроГруппа: Прочее

Берут, когда окно вызывается из кода.

Когда брать

  • окно вызывается из кода — из обработчика, из стора, из перехватчика запросов: разметки в шаблоне не нужно вовсе;
  • результат нужен значениемconfirm/prompt возвращают Promise, и ветвление пишется линейно;
  • окно открывается из места без шаблона.ts-модуль, роутер-гард, обработчик ошибки;
  • окон много и они одинаковы — общий хост вместо копии v-model в каждом компоненте.

Когда взять другое

НужноБерите
Окно открывается разметкой и живёт на экранеGrDialog
Подтверждение объявлено в шаблонеGrConfirmDialog
Запрос значения объявлен в шаблонеGrPromptDialog
Сообщение без вопросаGrToaster
Своя раскладка модального слояGrModal

Разрешение промиса

Высокоуровневые методы никогда не реджектят: отмена — штатный исход, а не исключение. confirm даёт boolean, promptstring | null, alertvoid. Нужна точная причина закрытия — open(), он резолвит { action: 'confirm' | 'cancel' | 'close', value? }.

Каждый промис дополнен методом close(): закрыть диалог из кода.

Асинхронное подтверждение

confirm и prompt принимают onConfirm, который получает контекст и может быть асинхронным. Пока он в полёте:

  • кнопка подтверждения показывает загрузку;
  • Esc и клик по бэкдропу отключены — случайное движение не должно оборвать операцию. Кнопка закрытия в шапке остаётся: из окна с зависшим запросом нужен явный выход;
  • диалог остаётся открытым, если колбэк вернул false, выставил ошибку или бросил исключение.

Контекст даёт value, signal (обрывается при закрытии диалога любым способом — кнопкой, close() промиса, closeAll()), setError / setFieldError / clearErrors, setRawError, setLoading, close.

setFieldError(field, message) адресный: ошибки складываются в карту по именам полей. GrPromptDialog показывает ошибку своего поля (value), а если запись одна — её, каким бы именем она ни была помечена.

Ошибки сервера

Вместо ручного разбора onConfirm передаёт сырой ответ (Response, ошибку axios/fetch, Error, строку, JSON) в ctx.setRawError. Сервис прогоняет его через ту же цепочку парсеров, что и GrResponseErrorBanner: общее сообщение рисуется в теле диалога, fieldErrors раскладываются по полям, окно остаётся открытым для повторной попытки. Исключение из onConfirm классифицируется автоматически, если ошибка не выставлена руками.

Цепочка по умолчанию — универсальное ядро (coreResponseErrorParsers): abort, network, HTTP-статус, plain-message. Они работают на транспортном уровне и не делают формат-специфичных допущений, поэтому не срабатывают ложно на почти-Laravel ответах. Серверо-специфичные парсеры (Laravel, RFC 7807, JSON:API) подключаются осознанно через errorParsers — массивом или builder-функцией, получающей именованные пресеты.

Контекст приложения

Хост — GrDialogServiceHost — монтируется отдельным render() в document.body, вне дерева компонентов. Его экспорт нужен плагину и тестам: ставить хост в шаблон руками не надо, сервис делает это сам.

Vue берёт provides только из appContext, куда попадает лишь app.provide(), — значения от <GrConfigProvider> до хоста не доходят. Поэтому конфиг и i18n захватываются в месте вызова и едут вместе с заявкой.

Приоритет: опция appContext вызова → явный setAppContext → автокэш из первого вызова useDialogService() внутри setup.

Готовый синглтон dialogService создаётся на импорте модуля, где inject не работает вовсе. Он берёт контекст последнего вызова useDialogService() из setup — этого хватает обычному приложению, но при двух поддеревьях с разными провайдерами выбор будет за последним. Нужна точность — вызывайте useDialogService() в setup или задайте setAppContext. Открытый без всякого контекста диалог в dev-сборке печатает предупреждение.

Изоляция приложений

app.use(granularityDialogServicePlugin)

Плагин даёт приложению собственную очередь и собственный хост и снимает их по app.unmount(). Он обязателен там, где приложений на странице несколько (микрофронтенды — иначе они делят одну очередь), и полезен при HMR, где контейнер прошлого приложения иначе остаётся висеть в document.body.

Без плагина сервис работает как раньше — на ленивом модульном состоянии; для обычного SPA это ровно то, что нужно.

Синглтон dialogService inject не умеет, поэтому состояние выбирает так: единственное зарегистрированное плагином — его; несколько — dev-предупреждение и модульный фолбэк, потому что выбрать за автора тут нечего. Второй случай лечится вызовом useDialogService() в setup нужного приложения.

teardownDialogService() остаётся ручным выходом и работает над тем же состоянием, что и сам сервис.

Очередь

Обычные вызовы сериализуются очередью FIFO: показывается голова, следующий диалог — после закрытия предыдущего. Три алерта из цикла не лягут стопкой, и за фокус-ловушку никто не конкурирует. closeAll() разбирает очередь в том же порядке FIFO — промисы резолвятся в порядке вызовов.

priority (по умолчанию 0) двигает заявку среди ожидающих: больше — раньше, при равенстве порядок вызовов. Показанное окно при этом не прерывается — выдёргивать фокус-ловушку из-под пользователя хуже, чем задержка на одно окно.

Завершить заявку могут трое: кнопка в окне, close() её промиса и closeAll(). Путь один и идемпотентный: повторное завершение — no-op, промис резолвится ровно один раз, а всё заведённое под заявку (подписка на внешний signal, AbortController) сворачивается, когда окно уходит с экрана.

Вложенные диалоги

Диалог, открытый из onConfirm другого диалога, показывается поверх него, а не встаёт в очередь:

await dialog.confirm('Удалить проект?', {
  async onConfirm() {
    return await dialog.confirm('Точно? Восстановить будет нечем.')
  },
})

Иначе вложенный вызов ждал бы в очереди того, кто ждёт его: внешнее окно зависало бы в загрузке, а промис не резолвился бы никогда. Стопка окон здесь — не украшение, а единственный способ не получить дедлок.

Esc, inert и возврат фокуса достаются даром от общего стека слоёв: закрывается верхнее окно, нижнее помечено inert, фокус возвращается в него.

Жизненный цикл и SSR

Хост монтируется лениво при первом вызове и живёт, пока живёт его состояние: с плагином — до app.unmount(), без плагина — до teardownDialogService().

Сервис клиент-only: без window/document методы бросают ошибку, а не работают вхолостую — иначе модульная очередь мутировалась бы на сервере и текла между запросами. Императивные вызовы прячьте за клиентской проверкой.

Доступность

Все гарантии — от переиспользуемого стека GrConfirmDialog/GrPromptDialog/ GrDialog/GrModal: единственный заголовок на окно, фокус-ловушка, возврат фокуса. Esc и бэкдроп следуют closeOnEsc/closeOnBackdrop и уходят в ветку cancel/close.

Установка

npm i @feugene/granularity

Импорт

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

API

API этого компонента ещё не посчитан: генератор витрины пока обходит только ядро. Пока его нет, справочник — в документации пакета.

Примеры 2

Link

Linkзависит от окружения витрины
<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

Linkзависит от окружения витрины
<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>

Документация компонентаВсе компоненты