GrDialogService
Берут, когда окно вызывается из кода.
Когда брать
- окно вызывается из кода — из обработчика, из стора, из перехватчика запросов: разметки в шаблоне не нужно вовсе;
- результат нужен значением —
confirm/promptвозвращают Promise, и ветвление пишется линейно; - окно открывается из места без шаблона —
.ts-модуль, роутер-гард, обработчик ошибки; - окон много и они одинаковы — общий хост вместо копии
v-modelв каждом компоненте.
Когда взять другое
| Нужно | Берите |
|---|---|
| Окно открывается разметкой и живёт на экране | GrDialog |
| Подтверждение объявлено в шаблоне | GrConfirmDialog |
| Запрос значения объявлен в шаблоне | GrPromptDialog |
| Сообщение без вопроса | GrToaster |
| Своя раскладка модального слоя | GrModal |
Разрешение промиса
Высокоуровневые методы никогда не реджектят: отмена — штатный исход, а не
исключение. confirm даёт boolean, prompt — string | null, alert —
void. Нужна точная причина закрытия — 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
<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
<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>